spring security整合JWT鉴权

关于JWT是什么就不赘述了,可以看看阮一峰大佬的文章:https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

定义JWT Body

首先需要定义一个Bean用来存放JWT Body,字段可以根据具体业务决定。这一步当然可以跳过,你可以用Map传值,只不过可维护性会差点

@Data
public class JwtBody {

    private String id;
    private String user;
    private String role;

}

编写JWT工具类

首先添加Maven依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

工具类

public abstract class JwtUtil {
	//传入JwtBody和密钥,生成Token返回,有效期两个小时
    @SuppressWarnings("unchecked")
    public static String generate(JwtBody claims, String secret) {
        final SecretKey secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        return Jwts.builder()
                .setExpiration(Date.from(ZonedDateTime.now().plusHours(2).toInstant()))
                .addClaims(BeanMap.create(claims))
                .signWith(secretKey).compact();
    }
	//传入Token和密钥,验证Token并解析JwtBody返回
    public static JwtBody parse(String token, String secret) {
        final SecretKey secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        final JwtBody claims = new JwtBody();
        final Claims body = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
        BeanMap.create(claims).putAll(body);
        return claims;
    }

}

两个方法,一个编码,一个解码

编写Login Controller

编写一个登录API接口,用来生成Token返回给客户端

@RestController
@RequiredArgsConstructor
@RequestMapping("")
@CrossOrigin
public class AuthController {

    private final AuthService authService;

    @PostMapping("/login")
    public UserLoginResponse login(@RequestBody UserLoginRequest userLoginRequest) {
        String token = authService.login(userLoginRequest.getUsername(), userLoginRequest.getPassword());
        return UserLoginResponse.builder().token(token).build();
    }

}

登录方法的实现

public String login(String username, String password) {
    User user = userService.getByUsername(username);
    if (!passwordEncoder.matches(password, user.getPassword())) {
        throw new BadCredentialsException("Password error");
    }

    JwtBody claims = new JwtBody() {
        {
            setId(user.getId().toString());
            setUser(user.getUsername());
            setRole(user.getRole().name());
        }
    };
    return JwtUtil.generate(claims, config.getJwtSecret())
}

编写JWT Filter

需要编写一个过滤器,添加到Spring Security过滤器链,用来验证Token授权

@Component
public class JwtFilter extends OncePerRequestFilter {

    private final static String TOKEN_PREFIX = "Bearer";
    private AuthenticationManager authenticationManager;
    //从Header中提取Token封装成JwtAuthentication传入AuthenticationManager进行验证,然后将验证之后的JwtAuthentication设置到Spring Security上下文
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (header != null && header.startsWith(TOKEN_PREFIX)) { //只有在Header中包含Token的时候才会进行授权
            final String token = header.replaceAll("^" + TOKEN_PREFIX, "").trim();
            Authentication auth = new JwtAuthentication(token);
            try {
                Authentication authResult = authenticationManager.authenticate(auth);
                SecurityContextHolder.getContext().setAuthentication(authResult);
            } catch (JwtException e) {
                logger.warn("Authentication failed: " + e.getLocalizedMessage());
            }
        }
        filterChain.doFilter(request, response);
    }

    @Autowired
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

}

JwtAuthentication实现

public class JwtAuthentication implements Authentication {

    @Getter
    private final String token;
    @Getter
    @Setter
    private User user;
    private Boolean isAuthenticated = false;

    public JwtAuthentication(String token) {
        this.token = token;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getAuthorities();
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getDetails() {
        return user;
    }

    @Override
    public Object getPrincipal() {
        return user.getUsername();
    }

    @Override
    public boolean isAuthenticated() {
        return isAuthenticated;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        this.isAuthenticated = isAuthenticated;
    }

    @Override
    public String getName() {
        return getPrincipal().toString();
    }

}

编写JwtAuthenticationProvider

这个类用来实现具体的Token验证操作

@Component
@RequiredArgsConstructor
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final Config config;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        JwtAuthentication jwtAuthentication = (JwtAuthentication) authentication;
        JwtBody claims;
        try {
            claims = JwtUtil.parse(jwtAuthentication.getToken(), config.getJwtSecret()); //解析Token失败,直接抛出异常,让Filter捕获
        } catch (SignatureException e) {
            throw new AccessDeniedException(e.getLocalizedMessage());
        }
        final User user = new User();
        user.setId(Long.valueOf(claims.getId().toString()));
        user.setUsername(claims.getUser());
        user.setRole(Role.valueOf(claims.getRole()));
        jwtAuthentication.setUser(user);
        jwtAuthentication.setAuthenticated(true); //认证完毕,将认证状态设为已认证
        return jwtAuthentication;
    }

    @Override
    public boolean supports(Class<?> authentication) { //此处表示只认证JwtAuthentication及其子类
        return JwtAuthentication.class.isAssignableFrom(authentication);
    }

}

编写Spring Security配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private JwtFilter jwtFilter;
    private JwtAuthenticationProvider jwtAuthenticationProvider;

    @Bean
    @Override //将AuthenticationManager作为Bean添加到Spring容器
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(jwtAuthenticationProvider); //添加JwtAuthenticationProvider
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().logout().disable().formLogin().disable() //启用CORS,关闭CSRF防御,关闭登出,关闭表单登录
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //使用无状态会话,即不使用cookie
                .authorizeRequests(request -> {
                    request.antMatchers("/api/users/*").hasAuthority(Role.ADMIN.name());
                    request.antMatchers("/login", "/register").permitAll(); //放行登陆、注册接口
                })
                .addFilterAt(jwtFilter, UsernamePasswordAuthenticationFilter.class); //将JwtFilter添加到过滤器链
    }
    //为了防止循环依赖注入,使用setter注入依赖,而不是构造器
    @Autowired
    public void setJwtFilter(JwtFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }

    @Autowired
    public void setJwtAuthenticationProvider(JwtAuthenticationProvider jwtAuthenticationProvider) {
        this.jwtAuthenticationProvider = jwtAuthenticationProvider;
    }

}