Spring Security Jwt學習

緣由

最近的項目裏遇到了spring security的內容。

通過查找資料,敲demo練習,做出瞭如下歸納總結。

實現spring-security-jwt

項目地址:https://github.com/ZhangLujie4/spring-security-jwt-demo

首先理一下大致思路(權限控制如何實現?)

  • 用戶登錄驗證賬號密碼,根據用戶生成jwt
  • 每次rest請求時會有filter對jwt進行解析驗證,成功後保存這個Authentication(這一步就是權限控制)
  • 可以根據這個Authentication獲得用戶的標識信息

核心類

1.security的配置類(繼承WebSecurityConfigurerAdapter)

在security的配置類裏面主要實現三個configure方法

  • configure(AuthenticationManagerBuilder auth) user-detail相關配置
  • configure(WebSecurity web) 一般用來設置不需要權限過濾的文件
  • configure(HttpSecurity http) 核心,用來進行權限控制,過濾設置等重要的配置

1.1 configure(AuthenticationManagerBuilder auth)

常用的userdetails的相關配置有:

/** planA **/:
@Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
    auth.userDetailsService( jwtUserDetailsService ) //jwtUserDetailsService爲自定義UserDetailsService(繼承於UserDetailsService)
        .passwordEncoder( passwordEncoder() );
}

/** planB **/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 使用自定義身份驗證組件
    auth.authenticationProvider(new CustomAuthenticationProvider(userDetailsService, bCryptPasswordEncoder));
}

1.2 configure(WebSecurity web)

在這裏可以舉個例子,在demo中並未用到:

@Override
    public void configure(WebSecurity web) throws Exception {
        // TokenAuthenticationFilter will ignore the below paths
        web.ignoring().antMatchers(
                HttpMethod.POST,
                "/auth/login"
        );
        web.ignoring().antMatchers(
                HttpMethod.GET,
                "/",
                "/webjars/**",
                "/*.html",
                "/favicon.ico",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js"
            );

    }

這裏可以提以下web.ignoring()和http的permitAll()的區別:

WebSecurity主要是配置跟web資源相關的,比如css、js、images等等,但是這個還不是本質的區別,關鍵的區別如下:

  • 前者設置後不經過spring security的過濾器
  • 後者還是需要經過spring security的過濾器(其中包含登錄的和匿名的)。

1.3 configure(HttpSecurity http)

我的demo中實現了第三個configure,具體如下:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .headers().frameOptions().disable().and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/api/user/**").permitAll()
            .antMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN")
            .and()
            .addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
    }

addFilterBefore主要用來實現自定義jwtfilter的設置。

當然也可以用apply()方法添加一些配置(其中包含jwtfilter)。

2.自定義的Filter

2.1

demo中的filter實現具體如下:

/**
 * @author tori
 * 2018/7/30 下午2:06
 */
public class JwtFilter extends GenericFilterBean {

    public static final String AUTH_HEADER = "Authentication";
    public static final String AUTH_PREFIX = "Bearer ";

    private TokenProvider tokenProvider;

    public JwtFilter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
        String jwt = getAuthHeader(httpServletRequest);
        if (StringUtils.hasText(jwt)) {
            if (tokenProvider.validateToken(jwt)) {
                Authentication authentication = tokenProvider.getAuthentication(jwt);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    private String getAuthHeader(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTH_HEADER);
        String token = null;
        if (bearerToken != null && bearerToken.startsWith(AUTH_PREFIX)) {
            token = bearerToken.substring(7);
        }
        return token;
    }
}

filter的作用:

  • 讀取到請求頭中的授權的token

  • 從token解析出Authentication對象,將其保存到上下文裏

    SecurityContextHolder.getContext().setAuthentication(authentication)

  • Authentication作爲之後判斷用戶權限的依據。

一般情況下我們自定義一個類實現Authentication接口,這樣我們就可以通過解析Authentication獲取到我們需要的成員屬性值了。(getPrincipal)

當然,你也可以選擇用其他方式實現自定義的Authentication

步驟一

一個UserDetails的實現類

public class User implements UserDetails {
    ......
}

步驟二

一個AbstractAuthenticationToken的繼承類

public class TokenBasedAuthentication extends AbstractAuthenticationToken {

    private String token;
    private final UserDetails principle;
    ......
}

因爲AbstractAuthenticationToken也是實現了Authentication,所以這個類就可以當做Authentication作爲jwt的解析對象。

補充

@Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
    auth.userDetailsService( jwtUserDetailsService )
        .passwordEncoder( passwordEncoder() );
}

這裏的jwtUserDetailsService是一個實現了UserDetailsService的類,可以在裏面做權限對象相關的操作(修改密碼,通過username查詢數據庫等)

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private AuthenticationManager authenticationManager;

這兩個是比較常用的自動裝載的bean

3.TokenProvider

這個當然是最重要的了,用來提供生成和解析token方法的類

生成token

public String createToken(UserAuthentication userAuthentication) {
        String authorities = userAuthentication.getAuthorities().stream()
                .map(auth -> auth.getAuthority()).collect(Collectors.joining(","));

    long now = (new Date()).getTime();

    return JwtFilter.AUTH_PREFIX
        + Jwts.builder().setId(String.valueOf(userAuthentication.getUserId()))
        .setSubject(JSON.toJSONString(userAuthentication.getPrincipal()))//在這裏可以存入序列化後的對象
        .claim(AUTHORITY_KEY, authorities)//這裏可以存入key和value,value代表權限role的list
        .signWith(SignatureAlgorithm.HS512, SECRET_KEY) //祕鑰和加密算法
        .setExpiration(new Date(now + EXPIRES_IN))//設置過期時間
        .compact();
}

解析token獲得Authentication

public Authentication getAuthentication(String jwt) {
    Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(jwt).getBody();
    Collection<? extends GrantedAuthority> authorities = Arrays.asList(claims.get(AUTHORITY_KEY).toString().split(","))
        .stream().map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());
    SecurityUser user = JSON.parseObject(claims.get(Claims.SUBJECT).toString(), SecurityUser.class);
    user.setAuthorities(authorities);
    UserAuthentication authentication = new UserAuthentication(user);

    return authentication;
}

我這裏是token可以解析出整個對象,你也可以用token僅僅解析出username(唯一標識),用於後續的授權處理操作。

以上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章