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登陆认证流程图
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设置成未认证,需要重走一遍认证流程
@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方法以及用户密码比较的方法
@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.测试
没有登陆是无法访问的
调用登录接口