| 集成了SpringSecurity后,又花了一天时间集成了JWT,记录一下。完整代码在我的Gitee仓库:FreeFancy
 0、一些背景HTTP是无状态的,我们不能确定两次请求是不是一个用户发出的,就必须要每次都进行认证。传统方法解决这个问题是采用session和cookie机制,虽然解决了问题,但有一些缺点: session通常是保存在内存中的,用户数量如果较多,服务器的压力大;因为session是保存在最初认证的那台服务器上,换了一台服务器又需要认证,不能很好的适应分布式的环境;有CSRF的风险。因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
 后来人们提出了基于token的鉴权机制,它不需要服务器去记录用户的认证或会话信息,而是将token交给客户端保存,每次的请求的时候携带,服务器只要鉴定即可,这样做有一些优点: 服务器开销小;不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利;因为不依赖于session,就杜绝了CSRF的风险。
 基于Token的鉴权机制应用最广的还是JWT(JSON WEB TOKEN)。这里我们不多解释JWT,相关文档很多请自动搜索。 1、实战前置工作这里是在SpringBoot已经简单整合了SpringSecurity的情况下开始整合JWT,这里前置工作有:因为是前后端分离的情况,我们统一返回JSON,所以实现了
 登录成功的自定义处理;登录失败的自定义处理;未登录被拦截的自定义处理。
 我们的用户信息是要在数据库中取的,所以我们要实现UserDetailsService接口,返回一个保存了用户信息的UserDetails对象,并在配置中注册。 部分配置文件 @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        http.exceptionHandling()
                .authenticationEntryPoint(customizeAuthenticationEntryPoint);
        
        http.formLogin()
                
                .successHandler(customizeAuthenticationSuccessHandler)
                
                .failureHandler(customizeAuthenticationFailureHandler);
        
        http.logout()
                
                .logoutSuccessHandler(customizeLogoutSuccessHandler);
        
        http.authorizeRequests()
                
                .antMatchers("/login", "/register").anonymous()
                
                .antMatchers(SWAGGER_SOURCE_PERMIT_ALL.split(",")).permitAll()
                .anyRequest().authenticated();
    }
 这里贴出其中一个自定义处理,其他都是类似的,返回相应结果的JSON信息。 
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
        ResponseJson responseJson = ResponseJson.failure(ResponseCode.USER_NOT_LOGGED_IN);
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(responseJson));
    }
}
 导包
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
 思路看了很多别人的博客,思路并不清晰,开始还以为这东西搞起来很复杂,最后弄下其实还是不难的,需要配置的东西并不多。 对于新登录的用户,我们只要在登录成功后将TOKEN返回!,原本的逻辑并不需要修改,只要在上面我们自定义登录成功的处理中返回生成好的TOKEN。对于登录过的用户发出的携带TOKEN的请求,我们要给它放行。对于没有携带TOKEN的用户请求(要拦截的路径),给它打回去。JWT TOKEN的生成和解析。
 思路是很自然的,得益于SpringSecurity实现起来也不难。 1、登录成功后返回TOKEN首先,/login路径的请求是不需要携带TOKEN就可以访问的。 http.authorizeRequests()
                
                .antMatchers("/login", "/register").anonymous()
 如果是表单登录,我们可以直接使用SpringSecurity自带的登录处理,请求参数中携带username,password,请求发给/login就可以了。还记得我们的登录成功自定义处理嘛?我们在里面把token生成然后返回就可以了。
 
        http.formLogin()
                
                .successHandler(customizeAuthenticationSuccessHandler)
                
                .failureHandler(customizeAuthenticationFailureHandler);
 @Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
    	
    	JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        StringBuilder authoritiesStr = new StringBuilder();
        for (GrantedAuthority authority : authorities) {
            authoritiesStr.append(authority.toString());
            authoritiesStr.append(JwtTokenUtils.SPLIT_CHAR);
        }
        String token = JwtTokenUtils.createToken(jwtUser.getId(), jwtUser.getUsername(), authoritiesStr.toString());
        
        HashMap<String, String> hashMap = new HashMap<>(1);
        hashMap.put("Authorization", token);
        ResponseJson responseJson = ResponseJson.success(hashMap);
        
        httpServletResponse.setContentType("text/json;charset=utf-8");
        
        httpServletResponse.getWriter().write(JSON.toJSONString(responseJson));
    }
}
 这样我们就完成了第一步。 2、携带TOKEN的请求,进行放行SpringSecurity本质是一个拦截器连(Filter Chain),我们需要自定义一个拦截器取处理请求中的TOKEN,并为请求放行。 
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String headerToken = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        
        if(headerToken == null || !headerToken.startsWith(JwtTokenUtils.TOKEN_PREFIX)){
            chain.doFilter(request, response);
            return;
        }
        
        if(!JwtTokenUtils.isExpiration(headerToken.replace(JwtTokenUtils.TOKEN_PREFIX,""))){
            
            CustomizeUsernamePasswordAuthenticationToken authentication = getAuthentication(headerToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        super.doFilterInternal(request, response, chain);
    }
	
    private CustomizeUsernamePasswordAuthenticationToken getAuthentication(String headerToken){
        String token = headerToken.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        Long userId = JwtTokenUtils.getUserId(token);
        String username = JwtTokenUtils.getUserName(token);
        
        String authoritiesStr = JwtTokenUtils.getUserAuthority(token);
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String s : authoritiesStr.split(JwtTokenUtils.SPLIT_CHAR)) {
            if (s != null && !"".equals(s)) {
                authorities.add(new SimpleGrantedAuthority(s));
            }
        }
        if(username != null){
            return new CustomizeUsernamePasswordAuthenticationToken(userId, username, null, authorities);
        }
        return null;
    }
}
 我们要将这个Filter注册进SpringSecurity里,框架留有接口,很方便,但我们应该放在哪里呢? 
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
 UsernamePasswordAuthenticationFilter是用来登录认证的,我们放在它前面,将TOKEN解析出的用户认证信息写进SecurityContext里,后续就可以放行了。另外这里提一嘴,因为基于TOKEN的认证鉴权不依赖于session,所以我们可以把session禁用了,当然也可以大胆的把csrf防护关闭。
 
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();
 3、拦截对于未携带TOKEN的请求不是登录请求,又未携带TOKEN信息,会被后续的拦截器拦截,肯定是进不到系统的,后续拦截器报错,我们只要处理异常即可。这里甚至不用特意处理,因为这和之前未登录拦截处理是一样的。
 
        http.exceptionHandling()
                .authenticationEntryPoint(customizeAuthenticationEntryPoint);
 @Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
        ResponseJson responseJson = ResponseJson.failure(ResponseCode.USER_NOT_LOGGED_IN);
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(responseJson));
    }
}
 4、生成和解析TOKEN生成是在成功登录后返回,解析是在处理携带TOKEN的请求,解析出用户的认证信息。那哪些信息要放在TOKEN里呢?username和authorities(权限信息)以及你想要的信息。
 我们对
 UserDetailsService的实现要求loadUserByUsername()返回一个UserDetails对象。 public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
 自带的对象就可以,它是UserDetails的实现类。 org.springframework.security.core.userdetails.User
 但它不能保存用户的ID,我就想把ID保存进TOKEN里,下次携带的时候可以解析出来,那我就自己写一个实现类。 
public class JwtUser implements UserDetails {
    private final Long id;
    private String password;
    private final String username;
    private final Set<GrantedAuthority> authorities;
    public JwtUser(Long id, String username, Set<GrantedAuthority> authorities){
        this.id = id;
        this.username = username;
        this.authorities = authorities;
    }
    public JwtUser(Long id, String username, String password, Set<GrantedAuthority> authorities){
        this.id = id;
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }
    public Long getId(){
        return id;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities(){
        return authorities;
    }
    @Override
    public String getPassword(){
        return password;
    }
    @Override
    public String getUsername(){
        return username;
    }
    @Override
    public boolean isAccountNonExpired(){
        return true;
    }
    @Override
    public boolean isAccountNonLocked(){
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired(){
        return true;
    }
    @Override
    public boolean isEnabled(){
        return true;
    }
}
 这个对象里的信息,就是我们要保存在TOKEN里的信息。然后我们需要生成和解析TOKEN的一个工具类,这里直接给出我的。
 
public class JwtTokenUtils {
    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String SECRET = "idea@whut.edu.cn";
    public static final String SPLIT_CHAR = ";";
    
    private static final Long EXPIRATION = 60 * 60 * 3L;
    private static final String AUTHORITY = "authority";
    private static final String ID = "id";
    public static String createToken(Long id, String username, String authority){
        HashMap<String, Object> map = new HashMap<>(2);
        map.put(ID, id);
        map.put(AUTHORITY, authority);
        return TOKEN_PREFIX + Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setClaims(map)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
                .compact();
    }
    
    public static Long getUserId(String token){
        Long id;
        try {
            id = (Long) getTokenBody(token).get(ID);
        } catch (Exception e){
            id = null;
        }
        return id;
    }
    
    public static String getUserName(String token){
        String username;
        try {
            username = getTokenBody(token).getSubject();
        } catch (Exception e){
            username = null;
        }
        return username;
    }
    public static String getUserAuthority(String token){
        return (String) getTokenBody(token).get(AUTHORITY);
    }
    private static Claims getTokenBody(String token){
        return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
    }
    
    public static boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }
}
 生成TOKEN用在: @Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
        JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        StringBuilder authoritiesStr = new StringBuilder();
        for (GrantedAuthority authority : authorities) {
            authoritiesStr.append(authority.toString());
            authoritiesStr.append(JwtTokenUtils.SPLIT_CHAR);
        }
        String token = JwtTokenUtils.createToken(jwtUser.getId(), jwtUser.getUsername(), authoritiesStr.toString());
        
        HashMap<String, String> hashMap = new HashMap<>(1);
        hashMap.put("Authorization", token);
        ResponseJson responseJson = ResponseJson.success(hashMap);
        
        httpServletResponse.setContentType("text/json;charset=utf-8");
        
        httpServletResponse.getWriter().write(JSON.toJSONString(responseJson));
    }
}
 解析TOKEN用在: public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String headerToken = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        
        if(headerToken == null || !headerToken.startsWith(JwtTokenUtils.TOKEN_PREFIX)){
            chain.doFilter(request, response);
            return;
        }
        
        if(!JwtTokenUtils.isExpiration(headerToken.replace(JwtTokenUtils.TOKEN_PREFIX,""))){
            
            CustomizeUsernamePasswordAuthenticationToken authentication = getAuthentication(headerToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        super.doFilterInternal(request, response, chain);
    }
    private CustomizeUsernamePasswordAuthenticationToken getAuthentication(String headerToken){
        String token = headerToken.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        Long userId = JwtTokenUtils.getUserId(token);
        String username = JwtTokenUtils.getUserName(token);
        
        String authoritiesStr = JwtTokenUtils.getUserAuthority(token);
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String s : authoritiesStr.split(JwtTokenUtils.SPLIT_CHAR)) {
            if (s != null && !"".equals(s)) {
                authorities.add(new SimpleGrantedAuthority(s));
            }
        }
        if(username != null){
            return new CustomizeUsernamePasswordAuthenticationToken(userId, username, null, authorities);
        }
        return null;
    }
}
 因为SecurityContextHolder.getContext().setAuthentication();要求传入Authentication的实现了,但框架自带的并不能携带我们要求携带的userId,所以自定义一个实现类。 
public class CustomizeUsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private final Long userId;
    private final Object principal;
    private Object credentials;
    public CustomizeUsernamePasswordAuthenticationToken(Long userId, Object principal, Object credentials) {
        super(null);
        this.userId = userId;
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }
    public CustomizeUsernamePasswordAuthenticationToken(Long userId, Object principal, Object credentials,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.userId = userId;
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
    public Long getUserId() {
        return this.userId;
    }
    @Override
    public Object getCredentials() {
        return this.credentials;
    }
    @Override
    public Object getPrincipal() {
        return this.principal;
    }
    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }
    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}
 5、使用SecurityContext中的信息每次请求,SecurityContext中保有我们放入的一小段信息,这份信息可以在该次请求的时候拿出来。 
public class SecurityUtils {
    public static Long getUserId(){
        Object token = SecurityContextHolder.getContext().getAuthentication();
        if (token instanceof CustomizeUsernamePasswordAuthenticationToken) {
            return ((CustomizeUsernamePasswordAuthenticationToken) token).getUserId();
        }
        return null;
    }
}
 到这里就完成了集成,捋清了思路其实还是很简单的,可以根据业务调整的地方有很多,可以在理解原理后按需修改。如有错误,恳请指出!
 |