web-security第五期:使用Spring Security+JWT實現基於令牌的訪問

源碼地址:鏈接 (Spring-Security)

前兩期分別分析了Spring Security Authentication 和 JWT,這一節組合這兩個技術,完成 記住我的功能

1.令牌工具類

使用上一期的知識,很容易寫一個下面的令牌操作工具類:

/**
 * 登錄令牌操作
 *
 * @author swing
 */
public class JwtService {
    /**
     * 令牌有效期(30分鐘)
     */
    private static final int EXPIRE_TIME = 1000 * 60 * 30;
    /**
     * 攜帶令牌信息的頭
     */
    private static final String TOKEN_HEADER = "Authorization";
    /**
     * 令牌前綴
     */
    private static final String TOKEN_PREFIX = "Bearer ";
    /**
     * 密鑰
     */
    private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    /**
     * 創建令牌
     *
     * @param userDO 用戶信息
     * @return 令牌
     */
    public static String createToken(UserDO userDO) {
        //設置令牌存儲的信息內容
        Map<String, Object> claims = new HashMap<>(2);
        claims.put("username", userDO.getUsername());
        claims.put("password", userDO.getPassword());
        //創建令牌
        return Jwts
                .builder()
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME))
                .signWith(SECRET_KEY)
                .compact();
    }

    /**
     * 解析token
     *
     * @param request 請求
     * @return token中的信息
     */
    public static Map<String, Object> resolverToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_HEADER);
        if (token != null && token.startsWith(TOKEN_PREFIX)) {
            token = token.replace(TOKEN_PREFIX, "");
            //解析token
            return Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }
        return null;
    }
}

2.登錄

public class LoginService {
    @Resource
    private AuthenticationManager authenticationManager;

    /**
     * 登錄認證
     *
     * @param userDO 用戶信息
     * @return token
     */
    public String login(UserDO userDO) {
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDO.getUsername(), userDO.getPassword()));
        return JwtService.createToken(userDO);
    }
}
/**
     * 驗證登錄信息
     *
     * @return 登錄結果
     */
    @PostMapping
    @ResponseBody
    RestResponse loginIn(@Validated @RequestBody UserDO userDO) {
        String token = loginService.login(userDO);
        Map<String, Object> body = new HashMap<>(1);
        body.put("token", token);
        return new RestResponse(HttpStatus.OK.value(), "認證成功!", body);
    }

我們將登錄成功的用戶信息(用戶名和密碼)存儲在token內(這是入門例子,正式開發不建議這麼做)

登錄成功響應結果如下:

{
  "status": 200,
  "msg": "認證成功!",
  "body": {
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6IjEyMzQ1NiIsInVzZXJuYW1lIjoic3dpbmciLCJleHAiOjE1OTE4NzEzNjl9.zVXr258NxQfS6KhYbnhA1pQHTn6fSNPmUaIV9K_ej9w"
  }
}

3.記住我

在第三期的內容中,我們知道用戶名和密碼的驗證實在 UsernamePasswordAuthenticationFilter  過濾器開始的,認證的結果是向SecurityContextHolder中填充值(Authorities) 的過程,如果SecurityContextHolder中被填充了Authorities,那麼此次請求就是被認證的請求,所以實現記住我的方法也很簡單,我們只需要在 UsernamePasswordAuthenticationFilter 之前自定義一個過濾器進行token的驗證,讓後完成和UsernamePasswordAuthenticationFilter 同樣的操作即可

過濾器代碼如下:

/**
 * 驗證該用戶是否認證
 *
 * @author swing
 */
@Component
public class AuthenticationFilter extends OncePerRequestFilter {
    @Resource
    private AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //清除之前遺留的認證信息
        SecurityContextHolder.clearContext();
        //獲取token中的信息
        Map<String, Object> claims = JwtService.resolverToken(request);
        if (claims != null) {
            Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(claims.get("username"), claims.get("password")));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}

然後在SecurityConfig中配置即可

protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                //允許匿名訪問的api,其他的需要驗證
                .antMatchers("/login", "/login/page").permitAll()
                // 除上面外的所有請求全部需要鑑權認證
                .anyRequest().authenticated();
        //將認證設置在UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

下次請求的時候帶上我們生產的token,如下例:

GET http://localhost:8080/file/3
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6IjEyMzQ1NiIsInVzZXJuYW1lIjoic3dpbmciLCJleHAiOjE1OTE4NzE4ODJ9.aUUz3hKle8FYNDW_aPlzHyeLIyO_JUfn27i2srORR9o

另外token是有過期時間點的,我們在創建令牌的時候聲明瞭它,當 jjwt 在解析令牌的時候,會根據當前系統的時間來判斷令牌是否過期,如果過期,則會拋出一個ExpiredJwtException,然後在web 層使用ExceptionHandler捕獲即可

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