本文会先从理论和实践两部分讲述如何去理解和实现通过JWT进行身份认证。
一、理论
1. SpringSecurity 默认的认证是需要通过 UsernamePasswordAuthenticationFilter 进行认证的,该过滤器认证前,会到 SecurityContextHolder 中寻找是否有符合的 Authentication 认证信息,如果有已认证的信息,则 UsernamePasswordAuthenticationFilter 可以直接拿到认证信息,进行后面的授权操作。
2. SecurityFilterChain 中允许我们使用 addFilterBefore 在某个过滤器前面添加自定义过滤器,所以我们可以自定义一个 JwtFilter 对用户信息进行授权认证,认证成功后将授权信息写入 SecurityContextHolder,这样在后面的 UsernamePasswordAuthenticationFilter 过滤器就能直接拿到授权信息了。
根据以上两点,最后能够实现先进行 JWT 验证,通过则授权;不通过则进行 UsernamePassword 验证。
二、实践
1. 首先引入 JWT
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.0.0</version>
</dependency>
2. 编写 JwtUtil 工具,用于加密和解密 token 信息
public class JwtUtil {
private static final String secret = "test123456";
public static String createToken(String userName, String authentications){
long expire = 7 * 24 * 3600 * 1000;
return JWT.create().withAudience(userName)
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis()+ expire))
.withClaim("authentications", authentications)
.sign(Algorithm.HMAC256(userName+secret));
}
public static boolean verifyToken(String token, String userName){
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(userName+secret)).build();
jwt = verifier.verify(token);
return true;
} catch (Exception e) {
System.out.println(e);
}
return false;
}
}
3. 实现 AuthticationSuccessHandler ,用于用户在第一次通过 UsernamePassword 验证的时候,返回一个 token
public class SuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) authentication;
String token = JwtUtil.createToken(user.getName(), user.getAuthorities().toString());
response.setContentType("text/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(token);
writer.flush();
writer.close();
}
}
4. 创建一个 JwtFilter ,主要是实现验证 token 授权信息并将授权信息写入 SecurityContextHolder 中
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = "";
String username = "";
for (Cookie cookie : request.getCookies()) {
if (cookie.getName().equals("username")) {
username = cookie.getValue();
}
if (cookie.getName().equals("token")) {
token = cookie.getValue();
}
}
if (JwtUtil.verifyToken(token, username)) {
DecodedJWT jwt = JwtUtil.decodedJWT(token, username);
String authentications = jwt.getClaims().get("authentications").toString();
String authentication = authentications.substring(2, authentications.length()-2);
User user = new User(username, "", AuthorityUtils.createAuthorityList(authentication));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else {
System.out.println(username + "JwtToken验证失败");
}
filterChain.doFilter(request, response);
}
}
5. 将 SuccessHandler 和 JwtFilter 配置到 SecurityFilterChain 中才能生效。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtFilter jwtFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/user/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(login -> login
.loginPage("/login")
.successHandler(new SuccessHandler())
.permitAll()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}
6. 最后测试一下,发送一个登录请求,返回 token 信息,
然后将 token 放在cookie 里面再请求一个需要权限才能访问的页面,正常返回
否则会重定向到登录页面