1. 程式人生 > 實用技巧 >Java必知必會-Spring Security

Java必知必會-Spring Security

目錄

一、Spring Security介紹

1、框架介紹

Spring 是一個非常流行和成功的 Java 應用開發框架。Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。一般來說,Web 應用的安全性包括使用者認證

Authentication)和使用者授權(Authorization****)兩個部分。

(1)使用者認證指的是:驗證某個使用者是否為系統中的合法主體,也就是說使用者能否訪問該系統。使用者認證一般要求使用者提供使用者名稱和密碼。系統通過校驗使用者名稱和密碼來完成認證過程。

(2)使用者授權指的是驗證某個使用者是否有許可權執行某個操作。在一個系統中,不同使用者所具有的許可權是不同的。比如對一個檔案來說,有的使用者只能進行讀取,而有的使用者可以進行修改。一般來說,系統會為不同的使用者分配不同的角色,而每個角色則對應一系列的許可權。

Spring Security其實就是用filter,多請求的路徑進行過濾。

(1)如果是基於Session,那麼Spring-security會對cookie裡的sessionid進行解析,找到伺服器儲存的sesion資訊,然後判斷當前使用者是否符合請求的要求。

(2)如果是token,則是解析出token,然後將當前請求加入到Spring-security管理的許可權資訊中去

2、認證與授權實現思路

如果系統的模組眾多,每個模組都需要授權與認證,所以我們選擇基於token的形式進行授權與認證,使用者根據使用者名稱密碼認證成功,然後獲取當前使用者角色的一系列許可權值,並以使用者名稱為key,許可權列表為value的形式存入redis快取中,根據使用者名稱相關資訊生成token返回,瀏覽器將token記錄到cookie中,每次呼叫api介面都預設將cookie中的token攜帶到header請求頭中,Spring-security解析header頭從而獲取token資訊,再解析token獲取當前使用者名稱,根據使用者名稱就可以從redis中獲取許可權列表,這樣Spring-security就能夠判斷當前請求是否有許可權訪問

Spring Security 支援兩種不同的認證方式:

  • 可以通過 form 表單來認證
  • 可以通過 HttpBasic 來認證

二、整合Spring Security

1、在common下建立spring_security模組

2、在spring_security引入相關依賴

<dependencies>
    <!-- Spring Security依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
    </dependency>
</dependencies>

\3、在service_acl引入**spring_security**依賴*

<dependency>
    <groupId>com.atguigu</groupId>
    <artifactId>spring_security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

0、程式碼結構說明:


4、建立spring security核心配置類TokenWebSecurityConfig

Spring Security的核心配置就是繼承WebSecurityConfigurerAdapter並註解@EnableWebSecurity的配置。

這個配置指明瞭使用者名稱密碼的處理方式、請求路徑的開合、登入登出控制等和安全相關的配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
    private UserDetailsService userDetailsService;
    private TokenManager tokenManager;
    private DefaultPasswordEncoder defaultPasswordEncoder;
    private RedisTemplate redisTemplate;
    @Autowired
    public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
                                  TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }
    /**
     * 配置設定
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthorizedEntryPoint())
                .and().csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and().logout().logoutUrl("/admin/acl/index/logout")
                .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()
                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
                .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
    }
    /**
     * 密碼處理
     * @param auth
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }
    /**
     * 配置哪些請求不攔截
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/api/**",
                "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
               );
    }
}

5、建立認證授權相關的工具類

(1)DefaultPasswordEncoder:密碼處理的方法

package com.atguigu.serurity.security;
import com.atguigu.commonutils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
 * <p>
 * 密碼的處理方法型別
 * </p>
 */
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
    public DefaultPasswordEncoder() {
        this(-1);
    }
    /**
     * @param strength
     *            the log rounds to use, between 4 and 31
     */
    public DefaultPasswordEncoder(int strength) {
    }
    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString()); // 密碼加密
    }
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString())); // 密碼認證
    }
}

(2)TokenManager:token操作的工具類

/**
 * <p>
 * token管理
 * </p>
 */
@Component
public class TokenManager {
    private long tokenExpiration = 24*60*60*1000;
    private String tokenSignKey = "123456";
    // 根據使用者名稱建立token值
    public String createToken(String username) {
        String token = Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
        return token;
    }
    // 獲取到請求頭的token,並解析獲取到對應使用者
    public String getUserFromToken(String token) {
        String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        return user;
    }
    public void removeToken(String token) {
        //jwttoken無需刪除,客戶端扔掉即可。
    }
}

(3)TokenLogoutHandler:退出實現

/**
 * <p>
 * 登出業務邏輯類
 * </p>
 */
public class TokenLogoutHandler implements LogoutHandler {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    
    public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }
    // 退出登入處理
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String token = request.getHeader("token");
        if (token != null) {
            tokenManager.removeToken(token);
            //清空當前使用者快取中的許可權資料
            String userName = tokenManager.getUserFromToken(token);
            redisTemplate.delete(userName);
        }
        ResponseUtil.out(response, R.ok());
    }
}

(4)UnauthorizedEntryPoint:未授權統一處理

import com.atguigu.commonutils.R;
import com.atguigu.commonutils.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * <p>
 * 未授權的統一處理方式
 * </p>
 */
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}

6、建立認證授權實體類

(1)SecutityUser

/**
 * <p>
 * 安全認證使用者詳情資訊
 * </p>
 */
@Data
@Slf4j
public class SecurityUser implements UserDetails {
    //當前登入使用者
    private transient User currentUserInfo;  // 禁止序列化user
    //當前許可權
    private List<String> permissionValueList;
    
    public SecurityUser() {
    }
    public SecurityUser(User user) {
        if (user != null) {
            this.currentUserInfo = user;
        }
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for(String permissionValue : permissionValueList) {
            if(StringUtils.isEmpty(permissionValue)) continue;
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
            authorities.add(authority);
        }
        return authorities; // 返回當前使用者許可權資訊
    }
    @Override
    public String getPassword() {
        return currentUserInfo.getPassword();
    }
    @Override
    public String getUsername() {
        return currentUserInfo.getUsername();
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

(2)User

/**
 * <p>
 * 使用者實體類
 * </p>
 */
@Data
@ApiModel(description = "使用者實體類")
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "微信openid")
    private String username;
    @ApiModelProperty(value = "密碼")
    private String password;
    @ApiModelProperty(value = "暱稱")
    private String nickName;
    @ApiModelProperty(value = "使用者頭像")
    private String salt;
    @ApiModelProperty(value = "使用者簽名")
    private String token;
}

7、建立認證和授權的filter

(1)TokenLoginFilter:認證的filter

/**
 * <p>
 * 登入過濾器,繼承UsernamePasswordAuthenticationFilter,對使用者名稱密碼進行登入校驗
 * </p>
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.authenticationManager = authenticationManager;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.setPostOnly(false);
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
    }
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 登入成功
     * @param req
     * @param res
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        SecurityUser user = (SecurityUser) auth.getPrincipal();
        String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
        redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
        ResponseUtil.out(res, R.ok().data("token", token));
    }
    /**
     * 登入失敗
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}

(2)TokenAuthenticationFilter:

/**
 * <p>
 * 訪問過濾器
 * </p>
 */
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
        super(authManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        logger.info("================="+req.getRequestURI());
        if(req.getRequestURI().indexOf("admin") == -1) {
            chain.doFilter(req, res);
            return;
        }
        UsernamePasswordAuthenticationToken authentication = null;
        try {
            authentication = getAuthentication(req);
        } catch (Exception e) {
            ResponseUtil.out(res, R.error());
        }
        if (authentication != null) {
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } else {
            ResponseUtil.out(res, R.error());
        }
        chain.doFilter(req, res);
    }
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置於header裡
        String token = request.getHeader("token");
        if (token != null && !"".equals(token.trim())) {
            String userName = tokenManager.getUserFromToken(token);
            List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            for(String permissionValue : permissionValueList) {
                if(StringUtils.isEmpty(permissionValue)) continue;
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
                authorities.add(authority);
            }
            if (!StringUtils.isEmpty(userName)) {
                return new UsernamePasswordAuthenticationToken(userName, token, authorities);
            }
            return null;
        }
        return null;
    }
}

底層原理

核心元件

  1. SecurityContextHolder:提供對SecurityContext的訪問
  2. SecurityContext,:持有Authentication物件和其他可能需要的資訊
  3. AuthenticationManager 其中可以包含多個AuthenticationProvider
  4. ProviderManager物件為AuthenticationManager介面的實現類
  5. AuthenticationProvider 主要用來進行認證操作的類 呼叫其中的authenticate()方法去進行認證操作
  6. Authentication:Spring Security方式的認證主體
  7. GrantedAuthority:對認證主題的應用層面的授權,含當前使用者的許可權資訊,通常使用角色表示
  8. UserDetails:構建Authentication物件必須的資訊,可以自定義,可能需要訪問DB得到
  9. UserDetailsService:通過username構建UserDetails物件,通過loadUserByUsername根據userName獲取UserDetail物件 (可以在這裡基於自身業務進行自定義的實現 如通過資料庫,xml,快取獲取等)

主要過濾器

想要對對Web資源進行保護,最好的辦法莫過於Filter,要想對方法呼叫進行保護,最好的辦法莫過於AOP。所以springSecurity在我們進行使用者認證以及授予許可權的時候,通過各種各樣的攔截器來控制權限的訪問,從而實現安全。

  1. ​ WebAsyncManagerIntegrationFilter
  2. ​ SecurityContextPersistenceFilter
  3. ​ HeaderWriterFilter
  4. ​ CorsFilter
  5. ​ LogoutFilter
  6. ​ RequestCacheAwareFilter
  7. ​ SecurityContextHolderAwareRequestFilter
  8. ​ AnonymousAuthenticationFilter
  9. ​ SessionManagementFilter
  10. ​ ExceptionTranslationFilter
  11. ​ FilterSecurityInterceptor
  12. ​ UsernamePasswordAuthenticationFilter
  13. ​ BasicAuthenticationFilter

自定義安全機制的載入機制

自定義了一個springSecurity安全框架的配置類 繼承WebSecurityConfigurerAdapter,重寫其中的方法configure

實現該類後,在web容器啟動的過程中該類例項物件會被WebSecurityConfiguration類處理。

  • 繼承WebSecurityConfigurerAdapter
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{

    //重寫了其中的configure()方法設定了不同url的不同訪問許可權
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/home", "/about","/img/*").permitAll()
                .antMatchers("/admin/**","/upload/**").hasAnyRole("ADMIN")
                .antMatchers("/order/**").hasAnyRole("USER","ADMIN")
                .antMatchers("/room/**").hasAnyRole("USER","ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and()
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler);
    }
}
  • WebSecurityConfiguration
    @Configuration
    public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
        private WebSecurity webSecurity;
        private Boolean debugEnabled;
        private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;
        private ClassLoader beanClassLoader;
       
       ...省略部分程式碼
    
        @Bean(
            name = {"springSecurityFilterChain"}
        )
        public Filter springSecurityFilterChain() throws Exception {
            boolean hasConfigurers = this.webSecurityConfigurers != null
             && !this.webSecurityConfigurers.isEmpty();
            if(!hasConfigurers) {
                WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)
                this.objectObjectPostProcessor
                  .postProcess(new WebSecurityConfigurerAdapter() {
                });
                this.webSecurity.apply(adapter);
            }
    
            return (Filter)this.webSecurity.build();
        }
        /*1、先執行該方法將我們自定義springSecurity配置例項
           (可能還有系統預設的有關安全的配置例項 ) 配置例項中含有我們自定義業務的許可權控制配置資訊
           放入到該物件的list陣列中webSecurityConfigurers中
           使用@Value註解來將例項物件作為形參注入
         */   
     @Autowired(
            required = false
        )
        public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object> 
        objectPostProcessor,
       @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") 
      List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers) 
    throws Exception {
        
        //建立一個webSecurity物件    
        this.webSecurity = (WebSecurity)objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));
            if(this.debugEnabled != null) {
                this.webSecurity.debug(this.debugEnabled.booleanValue());
            }
    
            //對所有配置類的例項進行排序
            Collections.sort(webSecurityConfigurers, WebSecurityConfiguration.AnnotationAwareOrderComparator.INSTANCE);
            Integer previousOrder = null;
            Object previousConfig = null;
    
    
            //迭代所有配置類的例項 判斷其order必須唯一
            Iterator var5;
            SecurityConfigurer config;
            for(var5 = webSecurityConfigurers.iterator(); var5.hasNext(); previousConfig = config) {
                config = (SecurityConfigurer)var5.next();
                Integer order = Integer.valueOf(WebSecurityConfiguration.AnnotationAwareOrderComparator.lookupOrder(config));
                if(previousOrder != null && previousOrder.equals(order)) {
                    throw new IllegalStateException("@Order on WebSecurityConfigurers must be unique. Order of " + order + " was already used on " + previousConfig + ", so it cannot be used on " + config + " too.");
                }
    
                previousOrder = order;
            }
    
            //將所有的配置例項新增到建立的webSecutity物件中
            var5 = webSecurityConfigurers.iterator();
            while(var5.hasNext()) {
                config = (SecurityConfigurer)var5.next();
                this.webSecurity.apply(config);
            }
            //將webSercurityConfigures 例項放入該物件的webSecurityConfigurers屬性中
            this.webSecurityConfigurers = webSecurityConfigurers;
        }
    }
    

SecurityContextHolder

這是一個工具類,只提供一些靜態方法。這個工具類的目的是用來儲存應用程式中當前使用人的安全上下文。

作用:保留系統當前的安全上下文細節,其中就包括當前使用系統的使用者的資訊。

(1)單機系統,即應用從開啟到關閉的整個生命週期只有一個使用者在使用。由於整個應用只需要儲存一個SecurityContext(安全上下文即可)

(2)多使用者系統,比如典型的Web系統,整個生命週期可能同時有多個使用者在使用。這時候應用需要儲存多個SecurityContext(安全上下文),需要利用ThreadLocal進行儲存,每個執行緒都可以利用ThreadLocal獲取其自己的SecurityContext,及安全上下文。

預設工作模式 MODE_THREADLOCAL

一個應用同時可能有多個使用者,每個使用者對應不同的安全上下文。預設情況下,SecurityContextHolder使用了ThreadLocal機制來儲存每個使用者的安全上下文。這意味著,只要針對某個使用者的邏輯執行都是在同一個執行緒中進行,即使不在各個方法之間以引數的形式傳遞其安全上下文,各個方法也能通過SecurityContextHolder工具獲取到該安全上下文。只要在處理完當前使用者的請求之後注意清除ThreadLocal中的安全上下文,這種使用ThreadLocal的方式是很安全的。當然在Spring Security中,這些工作已經被Spring Security自動處理,開發人員不用擔心這一點。

SecurityContextHolder基於ThreadLocal的工作方式天然很適合Servlet Web應用,因為預設情況下根據Servlet規範,一個Servlet request的處理不管經歷了多少個Filter,自始至終都由同一個執行緒來完成。

注意 : 這裡講的是一個Servlet request的處理不管經歷了多少個Filter,自始至終都由同一個執行緒來完成;而對於同一個使用者的不同Servlet request,它們在服務端被處理時,使用的可不一定是同一個執行緒(存在由同一個執行緒處理的可能性但不確保)。

其他工作模式

有一些應用並不適合使用ThreadLocal模式,那麼還能不能使用SecurityContextHolder了呢?答案是可以的。SecurityContextHolder還提供了其他工作模式。

比如有些應用,像Java Swing客戶端應用,它就可能希望JVM中所有的執行緒使用同一個安全上下文。此時我們可以在啟動階段將SecurityContextHolder配置成全域性策略MODE_GLOBAL

還有其他的一些應用會有自己的執行緒建立,並且希望這些新建執行緒也能使用建立者的安全上下文。這種效果,可以通過將SecurityContextHolder配置成MODE_INHERITABLETHREADLOCAL策略達到。

獲取當前使用者資訊

SecurityContextHolder中儲存的是當前訪問者的資訊。Spring Security使用一個Authentication物件來表示這個資訊。一般情況下,我們都不需要建立這個物件,在登入過程中,Spring Security已經建立了該物件並幫我們放到了SecurityContextHolder中。從SecurityContextHolder中獲取這個物件也是很簡單的。比如,獲取當前登入使用者的使用者名稱,可以這樣 :

// 獲取安全上下文物件,就是那個儲存在 ThreadLocal 裡面的安全上下文物件
// 總是不為null(如果不存在,則建立一個authentication屬性為null的empty安全上下文物件)
SecurityContext securityContext = SecurityContextHolder.getContext();

// 獲取當前認證了的 principal(當事人),或者 request token (令牌)
// 如果沒有認證,會是 null,該例子是認證之後的情況
Authentication authentication = securityContext.getAuthentication()

// 獲取當事人資訊物件,返回結果是 Object 型別,但實際上可以是應用程式自定義的帶有更多應用相關資訊的某個型別。
// 很多情況下,該物件是 Spring Security 核心介面 UserDetails 的一個實現類,你可以把 UserDetails 想像
// 成我們資料庫中儲存的一個使用者資訊到 SecurityContextHolder 中 Spring Security 需要的使用者資訊格式的
// 一個介面卡。
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
	String username = ((UserDetails)principal).getUsername();
} else {
	String username = principal.toString();
}

修改SecurityContextHolder的工作模式

綜上所述,SecurityContextHolder可以工作在以下三種模式之一:

  • MODE_THREADLOCAL (預設工作模式)

  • MODE_GLOBAL

  • MODE_INHERITABLETHREADLOCAL

    修改SecurityContextHolder的工作模式有兩種方法 :

    • SecurityContextHolder會自動從該系統屬性中嘗試獲取被設定的工作模式:設定一個系統屬性(system.properties) : spring.security.strategy;

    • 程式化方式主動設定工作模式的方法:呼叫SecurityContextHolder靜態方法setStrategyName()

原始碼

/**
 * 將一個給定的SecurityContext繫結到當前執行執行緒。
 */
public class SecurityContextHolder {
	// ~ Static fields/initializers
	// =====================================================================================
	// 三種工作模式的定義,每種工作模式對應一種策略
	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
	public static final String MODE_GLOBAL = "MODE_GLOBAL";

	// 類載入時首先嚐試從環境屬性中獲取所指定的工作模式
	public static final String SYSTEM_PROPERTY = "spring.security.strategy";	
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
	private static SecurityContextHolderStrategy strategy;

	// 初始化計數器,初始為0,
	// 1. 類載入過程中會被初始化一次,此值變為1
	// 2. 此後每次呼叫 setStrategyName 會對新的策略物件執行一次初始化,相應的該值會增1
	private static int initializeCount = 0;

	static {
		initialize();
	}
	// ~ Methods
	// =====================================================================================

	/**
	 * Explicitly clears the context value from the current thread.
	 */
	public static void clearContext() {
		strategy.clearContext();
	}

	/**
	 * Obtain the current SecurityContext.
	 *
	 * @return the security context (never null)
	 */
	public static SecurityContext getContext() {
		return strategy.getContext();
	}

	/**
	 * Primarily for troubleshooting purposes, this method shows how many times the class
	 * has re-initialized its SecurityContextHolderStrategy.
	 *
	 * @return the count (should be one unless you've called
	 * #setStrategyName(String) to switch to an alternate strategy.
	 */
	public static int getInitializeCount() {
		return initializeCount;
	}

	private static void initialize() {
		if (!StringUtils.hasText(strategyName)) {
			// Set default, 設定預設工作模式/策略 MODE_THREADLOCAL
			strategyName = MODE_THREADLOCAL;
		}

		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
		}
		else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
		}
		else if (strategyName.equals(MODE_GLOBAL)) {
			strategy = new GlobalSecurityContextHolderStrategy();
		}
		else {
			// Try to load a custom strategy
			try {
				Class<?> clazz = Class.forName(strategyName);
				Constructor<?> customStrategy = clazz.getConstructor();
				strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
			}
			catch (Exception ex) {
				ReflectionUtils.handleReflectionException(ex);
			}
		}

		initializeCount++;
	}

	/**
	 * Associates a new SecurityContext with the current thread of execution.
	 *
	 * @param context the new SecurityContext (may not be null)
	 */
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}

	/**
	 * Changes the preferred strategy. Do NOT call this method more than once for
	 * a given JVM, as it will re-initialize the strategy and adversely affect any
	 * existing threads using the old strategy.
	 *
	 * @param strategyName the fully qualified class name of the strategy that should be
	 * used.
	 */
	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}

	/**
	 * Allows retrieval of the context strategy. See SEC-1188.
	 *
	 * @return the configured strategy for storing the security context.
	 */
	public static SecurityContextHolderStrategy getContextHolderStrategy() {
		return strategy;
	}

	/**
	 * Delegates the creation of a new, empty context to the configured strategy.
	 */
	public static SecurityContext createEmptyContext() {
		return strategy.createEmptyContext();
	}

	public String toString() {
		return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount="
				+ initializeCount + "]";
	}
}


由原始碼可知,SecurityContextHolder利用了一個SecurityContextHolderStrategy(儲存策略)進行上下文的儲存。

SecurityContestHolderStrategy只是一個介面,這個介面提供建立、清空、獲取、設定上下文的操作。

  1. GlobalSecurityContextHolderStrategy: 全域性的上下文存取策略,只儲存一個上下文,對應前面說的單機系統。

  2. ThreadLocalSecurityContextHolderStrategy: ThreadLocal內部會用陣列來儲存多個物件的。原理是,ThreadLocal會為每個執行緒開闢一個儲存區域,來儲存相應的物件。

Authentication——使用者資訊的表示:

在SecurityContextHolder中儲存了當前與系統互動的使用者的資訊。Spring Security使用一個Authentication 物件來表示這些資訊。一般不需要自己建立這個物件,但是查詢這個物件的操作對使用者來說卻非常常見。

Principal(準則)=> 允許通過的規則,即允許訪問的規則,基本等價於UserDetails(使用者資訊)

SecurityContext(安全上下文)只是儲存了Authentication(認證資訊)主要包含了以下內容

  • 使用者許可權集合 => 可用於訪問受保護資源時的許可權驗證
  • 使用者證書(密碼) => 初次認證的時候,進行填充,認證成功後將被清空
  • 細節 => 暫不清楚,猜測應該是記錄哪些保護資源已經驗證授權,下次不用再驗證,等等。
  • Pirncipal => 大概就是賬號吧
  • 是否已認證成功

SpringSecurity底層認證流程