SpringSecurity结合jwt完成登陆认证

SpringBoot结合SpringSecurity、Jwt完成登陆认证

1.准备工作导入相关依赖

<dependencies>   
	<dependency>       
    <groupId>org.springframework.boot</groupId>        
    <artifactId>spring-boot-starter-security</artifactId>    
    </dependency>
    
    <dependency>        
    <groupId>org.springframework.boot</groupId>        
    <artifactId>spring-boot-starter-web</artifactId>    
    </dependency>    
    <dependency>  
    
    <groupId>org.springframework.boot</groupId>        
    <artifactId>spring-boot-devtools</artifactId>        
    <scope>runtime</scope>        
    <optional>true</optional>    
    </dependency>  
    
    <dependency>        
    <groupId>org.projectlombok</groupId>      
    <artifactId>lombok</artifactId>     
    <optional>true</optional>    
    </dependency> 
    
    <dependency>        
    <groupId>io.jsonwebtoken</groupId>        
    <artifactId>jjwt</artifactId>        
    <version>0.9.1</version>    
    </dependency> 
    
    <dependency>        
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId> 
    <version>4.6.1</version>   
    </dependency> 
    
    <dependency>        
    <groupId>com.alibaba</groupId>        
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.22</version>    
    </dependency> 
    
    <dependency>        
    <groupId>org.mybatis.spring.boot</groupId>        
    <artifactId>mybatis-spring-boot-starter</artifactId>        		 
    <version>2.1.1</version>    
    </dependency>
    
    <dependency>       
    <groupId>mysql</groupId>    
    <artifactId>mysql-connector-java</artifactId>        
    <version>8.0.18</version>    
    </dependency>

2.SpringSecurity登陆认证流程图

SpringSecurity结合jwt完成登陆认证_第1张图片
3.简单的SpringSecurity认证demo

​ 3.1. jwtTokenUtils

public class JwtTokenUtil {  
    private static final String CLAIM_KEY_USERNAME = "sub";  
    private static final String CLAIM_KEY_CREATED = "created";    	     
    @Value("${jwt.secret}")    
    private String secret;   
    @Value("${jwt.expiration}")    
    private Long expiration;    
    @Value("${jwt.tokenHead}")    
    private String tokenHead;   
    /**     * 根据负责生成JWT的token     */    
    private String generateToken(Map<String, Object> claims) {   
        return Jwts.builder()      
            .setClaims(claims)    
            .setExpiration(generateExpirationDate()) 
            .signWith(SignatureAlgorithm.HS512, secret)  
            .compact(); 
    } 
    /**     
    * 从token中获取JWT中的负载     
    */    
    private Claims getClaimsFromToken(String token) {       
        Claims claims = null;        
        try {            
            claims = Jwts.parser()
                .setSigningKey(secret) 
                .parseClaimsJws(token)               
                .getBody();        
        } catch (Exception e) {        
            log.info("JWT格式验证失败:{}", token);       
        }        
        return claims;    
    }    
    /**    
    * 生成token的过期时间   
    */    
    private Date generateExpirationDate() {   
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }    
    /**    
    * 从token中获取登录用户名  
    */    
    public String getUserNameFromToken(String token) {    
        String username;     
        try {           
            Claims claims = getClaimsFromToken(token);   
            username = claims.getSubject();     
        } catch (Exception e) {  
            username = null;     
        }        
        return username; 
    }    
    /**     
    * 验证token是否还有效          
    * @param token      
    *客户端传入的token    
    * @param userDetails 
    从数据库中查询出来的用户信息     
    */   
    public boolean validateToken(String token, UserDetails userDetails)
    {        
        String username = getUserNameFromToken(token); 
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);  
    }   
    /**    
    * 判断token是否已经失效    
    */
    private boolean isTokenExpired(String token) {      
        Date expiredDate = getExpiredDateFromToken(token); 
        return expiredDate.before(new Date());   
    }    
    /**   
    * 从token中获取过期时间   
    */   
    private Date getExpiredDateFromToken(String token) {   
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration(); 
    }    
    /**
    * 根据用户信息生成token  
    */    
    public String generateToken(UserDetails userDetails) { 
        Map<String, Object> claims = new HashMap<>();        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); 
        claims.put(CLAIM_KEY_CREATED, new Date());   
        return generateToken(claims);   
    }   
    /**     
    * 当原来的token没过期时是可以刷新的  
    *     
    * @param oldToken 带tokenHead的token    
    */    public String refreshHeadToken(String oldToken) {
        if(StringUtils.isEmpty(oldToken)){    
            return null;   
        }       
        String token = oldToken.substring(tokenHead.length());        
        if(StringUtils.isEmpty(token)){    
            return null;  
        }       
        //token校验不通过       
        Claims claims = getClaimsFromToken(token);  
        if(claims==null){     
            return null;     
        }        
        //如果token已经过期,不支持刷新
        if(isTokenExpired(token)){    
            return null;    
        }       
        //如果token在30分钟之内刚刷新过,返回原token 
        if(tokenRefreshJustBefore(token,30*60)){ 
            return token;    
        }else{            
            claims.put(CLAIM_KEY_CREATED, new Date());     
            return generateToken(claims);      
        }    
    }    
    /**     
    * 判断token在指定时间内是否刚刚刷新过   
    * @param token 原token    
    * @param time 指定时间(秒)   
    */   
    private boolean tokenRefreshJustBefore(String token, int time) {        
        Claims claims = getClaimsFromToken(token);        
        Date created = claims.get(CLAIM_KEY_CREATED, Date.class);   
        Date refreshDate = new Date();      
        //刷新时间在创建时间的指定时间内
        
   if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){            
       return true;  
  		}        
        return false;  
    }
}

3.2.实现一个jwt过滤器

此过滤器

public class JwtFilter  extends OncePerRequestFilter {    @Value("${jwt.tokenHeader}")    
 private String tokenHeader;   
 @Value("${jwt.tokenHead}")   
 private String tokenHead;    @Autowired    private JwtTokenUtil jwtTokenUtil;    @Autowired    
 private UserDetailsService userDetailsService;    
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { 
    //获取jwt信息
    //校验头一般为 Authentication Bearer token
    String authHead = request.getHeader(this.tokenHeader);       	    
    if(!StringUtils.isEmpty(authHead) && authHead.startsWith(this.tokenHead)){            //获取token            
        String jwtToken = authHead.substring(this.tokenHead.length());                   String userNameFromToken = jwtTokenUtil.getUserNameFromToken(jwtToken);            log.info("username:{}",userNameFromToken); 
        if (!StringUtils.isEmpty(userNameFromToken) && 
            SecurityContextHolder.getContext().getAuthentication() == null) {                UserDetails userDetails = 
            this.userDetailsService.loadUserByUsername(userNameFromToken);                //用户校验      
 		if (jwtTokenUtil.validateToken(jwtToken,userDetails)) {                    
     	UsernamePasswordAuthenticationToken authRequest = new 
        UsernamePasswordAuthenticationToken(userDetails, null, 
		Collections.singletonList(new SimpleGrantedAuthority("admin")));
     	authRequest.setDetails(new                            		 WebAuthenticationDetailsSource().buildDetails(request));
     SecurityContextHolder.getContext().setAuthentication(authRequest);             	}  
                                                                            }       	 }        
    filterChain.doFilter(request,httpServletResponse); 
	}
}

过滤器可以参考默认的用户密码过滤器UsernamePasswordAuthenticationFilter的校验过程,但是继承的父类不一样,jwtFilter继承的是OncePerRequestFilter,保证一次请求只通过一次Filter,由于笔者的表中没有用户角色表,UsernamePasswordAuthenticationToken 对象的authorities没有传入,导致获取token后访问接口都是403,导致这种问题笔者怀疑是UsernamePasswordAuthenticationToken 有authorities的构造函数会将用户设置成已认证,但是不带authorities设置成未认证,需要重走一遍认证流程

SpringSecurity结合jwt完成登陆认证_第2张图片

SpringSecurity结合jwt完成登陆认证_第3张图片

SpringSecurity结合jwt完成登陆认证_第4张图片
3.3.SpringSecurityConfig

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {   
    @Override  
    public void configure(WebSecurity web) throws Exception {       
        web.ignoring().antMatchers("/favicon.co"); 
                                                            }   
    @Bean    
    public PasswordEncoder passwordEncoder() {  
        return new BCryptPasswordEncoder(); 
    }    
    @Bean 
    public JwtFilter jwtAuthenticationTokenFilter(){ 
        return new JwtFilter();   
    }   
    @Autowired   
    private UserServiceImpl userService;   
    @Bean    
    public JwtTokenUtil tokenUtil(){     
        return new JwtTokenUtil();  
    }   
    @Bean   
    @Override    
    public AuthenticationManager authenticationManagerBean() throws Exception {       				 return super.authenticationManagerBean();  
                                                                              }    
    /**    
    *配置权限路径  
    * @param http   
    * @throws Exception  
    */    
    @Override   
    protected void configure(HttpSecurity http) throws Exception {       
        http.authorizeRequests() 
            //login路径允许访问
            .antMatchers("/login").permitAll()  
            //option请求都放行
            .antMatchers( HttpMethod.OPTIONS, "/**").permitAll()  
            //其他的任意请求都需要认证
            .anyRequest()               
            .authenticated()                
            .and() 
            //关闭csrf
            .csrf()           
            .disable()  
            //关闭自带的session存储,因为现在没有用session保存用户信息
            .sessionManagement()                
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)                
           	// 自定义权限拦截器JWT过滤器            
            .and()               	.addFilterBefore(jwtAuthenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);    
    }   
    @Override   
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
    {        
        //数据库认证,配置密码加密方式
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());    }
}

3.4.UserService,需要实现UserDetailsService接口,重写loadUserByUsername这个方法,SpringSecurity认证会调用loadUserByUsername,DaoAuthenticationProvider源码中看到加载loadUserByName方法以及用户密码比较的方法

SpringSecurity结合jwt完成登陆认证_第5张图片
SpringSecurity结合jwt完成登陆认证_第6张图片

@Service
@Slf4j
public class UserServiceImpl implements UserDetailsService , UserService {    
    @Autowired
    private UserMapper userMapper; 
    @Autowired   
    private PasswordEncoder passwordEncoder;   
    @Autowired   
    private JwtTokenUtil tokenUtil; 
    @Override   
    public UserDetails loadUserByUsername(String userName) throws 
        UsernameNotFoundException {        
        if (StringUtils.isEmpty(userName)) {   
            throw new UsernameNotFoundException("用户不存在");  
        }        
        return userMapper.getUser(userName);  
    }    
    @Override    
    public String login(String username, String password) {   
        //String encode = passwordEncoder.encode(password);
        // log.info(encode);  
        UserDetails userDetails = userMapper.getUser(username);   
        String token = null;     
        try {           
            if (userDetails!=null && passwordEncoder.matches(password,userDetails.getPassword())){   
                //生成token   
                UsernamePasswordAuthenticationToken 
                    usernamePasswordAuthenticationToken = new 
                    UsernamePasswordAuthenticationToken(username, null);         SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);              
                token = tokenUtil.generateToken(userDetails); 
                return token;          
            }else         
                throw new BadCredentialsException("密码不正确");
        } catch (AuthenticationException e) {  
            e.printStackTrace();        
            log.info("登陆异常:{}",e.getMessage());   
        }       
        return token; 
    }
}

3.5. UserMapper

public interface UserMapper {    
    @Select("select * from users where username = #{username}")  
    Users getUser(@Param("username") String username); 

3.6.entity

public class Users implements Serializable, UserDetails { 

    @Getter
    @Setter 
    private long id;  
    @Setter 
    private String username; 
    @Setter  
    private String password;
    @Getter 
    @Setter 
    private String userSex; 
    @Getter 
    @Setter  
    private String nickName;   
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() { 
        return Collections.singletonList(new SimpleGrantedAuthority("admin"));
    }  
    @JsonIgnore 
    @Override  
    public String getPassword() {    
        return password;  }  
    @Override  
    public String getUsername() { 
        return username;  } 
    @JsonIgnore 
    @Override  
    public boolean isAccountNonExpired() { 
        return true; 
    }  
    @JsonIgnore
    @Override 
    public boolean isAccountNonLocked() {    
        return true;  
    }  @JsonIgnore  
    @Override  public boolean isCredentialsNonExpired() {   
        return true; 
    }  
    @Override 
    public boolean isEnabled() {  
        return true; 
    }
}

3.7.application.yml

jwt:  
tokenHeader: Authorization #JWT存储的请求头
secret: test #JWT加解密使用的密钥 
expiration: 604800 #JWT的超期限时间(60*60*24)  
tokenHead: Bearer  #JWT负载中拿到开头 
datasource:   
druid:   
username: root  
password: 123456 
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true      
driver-class-name: com.mysql.cj.jdbc.Driver

4.测试

SpringSecurity结合jwt完成登陆认证_第7张图片

没有登陆是无法访问的

调用登录接口

SpringSecurity结合jwt完成登陆认证_第8张图片
接下来再次访问接口
SpringSecurity结合jwt完成登陆认证_第9张图片
访问成功

你可能感兴趣的:(SpringSecurity结合jwt完成登陆认证)