本文会先从理论和实践两部分讲述如何去理解和实现通过JWT进行身份认证。

一、理论

1. SpringSecurity 默认的认证是需要通过 UsernamePasswordAuthenticationFilter 进行认证的,该过滤器认证前,会到 SecurityContextHolder 中寻找是否有符合的 Authentication 认证信息,如果有已认证的信息,则 UsernamePasswordAuthenticationFilter 可以直接拿到认证信息,进行后面的授权操作。

2. SecurityFilterChain 中允许我们使用 addFilterBefore 在某个过滤器前面添加自定义过滤器,所以我们可以自定义一个 JwtFilter 对用户信息进行授权认证,认证成功后将授权信息写入 SecurityContextHolder,这样在后面的 UsernamePasswordAuthenticationFilter 过滤器就能直接拿到授权信息了。

根据以上两点,最后能够实现先进行 JWT 验证,通过则授权;不通过则进行 UsernamePassword 验证。

3. 最后要补充一句,在数据库里面保存的权限,一定要以ROLE_开头,比如“ROLE_ADMIN”,只是“ADMIN”的话,最后 SpringSecurity 也会给你返回一个 403 错误。

二、实践

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 = "secret";

    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 DecodedJWT decodedJWT(String token, String username) {
        DecodedJWT jwt = null;
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256(username + secret)).build();
        jwt = verifier.verify(token);
        return jwt;
    }

    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(username + " 验证失败");
        }
        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 里面再请求一个需要权限才能访问的页面,正常返回

否则会重定向到登录页面

Logo

一站式 AI 云服务平台

更多推荐