SpringSecurity代碼實現JWT接口權限授予與校驗

經過筆者前兩篇文章的說明,相信你們已經知道JWT是什麼,怎麼用,該如何結合Spring Security使用。那麼本節就用代碼來具體的實現一下JWT登陸認證及鑑權的流程。前端

1、環境準備工做

  • 創建Spring Boot項目並集成了Spring Security,項目能夠正常啓動
  • 經過controller寫一個HTTP的GET方法服務接口,好比:「/hello」
  • 實現最基本的動態數據驗證及權限分配,即實現UserDetailsService接口和UserDetails接口。這兩個接口都是向Spring Security提供用戶、角色、權限等校驗信息的接口
  • 若是你學習過Spring Security的formLogin登陸模式,請將HttpSecurity配置中的formLogin()配置段所有去掉。由於JWT徹底使用JSON接口,沒有from表單提交。
  • HttpSecurity配置中必定要加上csrf().disable(),即暫時關掉跨站攻擊CSRF的防護。這樣是不安全的,咱們後續章節再作處理。

以上的內容,咱們在以前的文章中都已經講過。若是仍然不熟悉,能夠翻看本號以前的文章。web

2、開發JWT工具類

經過maven座標引入JWT工具包jjwtspring

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>複製代碼

在application.yml中加入以下自定義一些關於JWT的配置數據庫

jwt: 
  header: JWTHeaderName
  secret: aabbccdd  
  expiration: 3600000   複製代碼
  • 其中header是攜帶JWT令牌的HTTP的Header的名稱。雖然我這裏叫作JWTHeaderName,可是在實際生產中可讀性越差越安全。
  • secret是用來爲JWT基礎信息加密和解密的密鑰。雖然我在這裏在配置文件寫死了,可是在實際生產中一般不直接寫在配置文件裏面。而是經過應用的啓動參數傳遞,而且須要按期修改。
  • expiration是JWT令牌的有效時間。

寫一個Spring Boot配置自動加載的工具類。json

@Data
@ConfigurationProperties(prefix = "jwt")    //配置自動加載,prefix是配置的前綴
@Component
public class JwtTokenUtil implements Serializable {

    private String secret;
    private Long expiration;
    private String header;


    /**
     * 生成token令牌
     *
     * @param userDetails 用戶
     * @return 令token牌
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        return generateToken(claims);
    }

    /**
     * 從令牌中獲取用戶名
     *
     * @param token 令牌
     * @return 用戶名
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 判斷令牌是否過時
     *
     * @param token 令牌
     * @return 是否過時
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 驗證令牌
     *
     * @param token       令牌
     * @param userDetails 用戶
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        SysUser user = (SysUser) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }


    /**
     * 從claims生成令牌,若是看不懂就看誰調用它
     *
     * @param claims 數據聲明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
        return Jwts.builder().setClaims(claims)
                            .setExpiration(expirationDate)
                            .signWith(SignatureAlgorithm.HS512, secret)
                            .compact();
    }

    /**
     * 從令牌中獲取數據聲明,若是看不懂就看誰調用它
     *
     * @param token 令牌
     * @return 數據聲明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

}複製代碼

上面的代碼就是使用io.jsonwebtoken.jjwt提供的方法開發JWT令牌生成、刷新的工具類。後端

3、開發登陸接口(獲取Token的接口)

  • "/authentication"接口用於登陸驗證,而且生成JWT返回給客戶端
  • "/refreshtoken"接口用於刷新JWT,更新JWT令牌的有效期
@RestController
public class JwtAuthController {

    @Resource
    private JwtAuthService jwtAuthService;

    @PostMapping(value = "/authentication")
    public AjaxResponse login(@RequestBody Map<String, String> map) {
        String username = map.get("username");
        String password = map.get("password");
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            return AjaxResponse.error(
                new CustomException(CustomExceptionType.USER_INPUT_ERROR,"用戶名密碼不能爲空"));
        }
        return AjaxResponse.success(jwtAuthService.login(username, password));
    }

    @PostMapping(value = "/refreshtoken")
    public AjaxResponse refresh(@RequestHeader("${jwt.header}") String token) {
        return AjaxResponse.success(jwtAuthService.refreshToken(token));
    }

}複製代碼

核心的token業務邏輯寫在JwtAuthService 中安全

  • login方法中首先使用用戶名、密碼進行登陸驗證。若是驗證失敗拋出BadCredentialsException異常。若是驗證成功,程序繼續向下走,生成JWT響應給前端
  • refreshToken方法只有在JWT token沒有過時的狀況下才能刷新,過時了就不能刷新了。須要從新登陸。
@Service
public class JwtAuthService {
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private JwtTokenUtil jwtTokenUtil;

    public String login(String username, String password) {
        //使用用戶名密碼進行登陸驗證
        UsernamePasswordAuthenticationToken upToken = 
                    new UsernamePasswordAuthenticationToken( username, password );
        Authentication authentication = authenticationManager.authenticate(upToken);  
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //生成JWT
        UserDetails userDetails = userDetailsService.loadUserByUsername( username );
        return jwtTokenUtil.generateToken(userDetails);
    }

    public String refreshToken(String oldToken) {
        if (!jwtTokenUtil.isTokenExpired(oldToken)) {
            return jwtTokenUtil.refreshToken(oldToken);
        }
        return null;
    }
}複製代碼

由於使用到了AuthenticationManager ,因此在繼承WebSecurityConfigurerAdapter的SpringSecurity配置實現類中,將AuthenticationManager 聲明爲一個Bean。並將"/authentication"和 "/refreshtoken"開放訪問權限,如何開放訪問權限,咱們以前的文章已經講過了。springboot

@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}複製代碼

4、接口訪問鑑權過濾器

當用戶第一次登錄以後,咱們將JWT令牌返回給了客戶端,客戶端應該將該令牌保存起來。在進行接口請求的時候,將令牌帶上,放到HTTP的header裏面,header的名字要和jwt.header的配置一致,這樣服務端才能解析到。下面咱們定義一個攔截器:session

  • 攔截接口請求,從請求request獲取token,從token中解析獲得用戶名
  • 而後經過UserDetailsService得到系統用戶(從數據庫、或其餘其存儲介質)
  • 根據用戶信息和JWT令牌,驗證系統用戶與用戶輸入的一致性,並判斷JWT是否過時。若是沒有過時,至此代表了該用戶的確是該系統的用戶。
  • 可是,你是系統用戶不表明你能夠訪問全部的接口。因此須要構造UsernamePasswordAuthenticationToken傳遞用戶、權限信息,並將這些信息經過authentication告知Spring Security。Spring Security會以此判斷你的接口訪問權限。
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private MyUserDetailsService userDetailsService;

    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
    
       // 從這裏開始獲取 request 中的 jwt token
        String authHeader = request.getHeader(jwtTokenUtil.getHeader());
        log.info("authHeader:{}", authHeader);
        // 驗證token是否存在
        if (authHeader != null && StringUtils.isNotEmpty(authHeader)) {
           // 根據token 獲取用戶名
            String username = jwtTokenUtil.getUsernameFromToken(authHeader);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                // 經過用戶名 獲取用戶的信息
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                
                // 驗證JWT是否過時
                if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
                    //加載用戶、角色、權限信息,Spring Security根據這些信息判斷接口的訪問權限
                    UsernamePasswordAuthenticationToken authentication 
                            = new UsernamePasswordAuthenticationToken(userDetails, null, 
                                                                      userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource()
                                            .buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}複製代碼

在spring Security的配置類(即WebSecurityConfigurerAdapter實現類的configure(HttpSecurity http)配置方法中,加入以下配置:app

.sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);複製代碼

  • 由於咱們使用了JWT,代表了咱們的應用是一個先後端分離的應用,因此咱們能夠開啓STATELESS禁止使用session。固然這並不絕對,先後端分離的應用經過一些辦法也是可使用session的,這不是本文的核心內容不作贅述。
  • 將咱們的自定義jwtAuthenticationTokenFilter,加載到UsernamePasswordAuthenticationFilter的前面。

5、測試一下:

測試登陸接口,即:獲取token的接口。輸入正確的用戶名、密碼便可獲取token。

file

下面咱們訪問一個咱們定義的簡單的接口「/hello」,可是不傳遞JWT令牌,結果是禁止訪問。當咱們將上一步返回的token,傳遞到header中,就能正常響應hello的接口結果。

file

期待您的關注

相關文章
相關標籤/搜索