项目码云:?https://gitee.com/kewen-yang/back2.git
1.Spring Security介绍:
????????Spring Security是基于Spring生态圈的,用于提供安全访问控制解决方案的框架。Spring Security的安 全管理有两个重要概念,分别是Authentication(认证)和Authorization(授权)。
????????为了方便Spring Boot项目的安全管理,Spring Boot对Spring Security安全框架进行了整合支持,并提 供了通用的自动化配置,从而实现了Spring Security安全框架中包含的多数安全管理功能。
????????Spring Security登录认证主要涉及两个重要的接口 UserDetailService和UserDetails接口。 UserDetailService接口主要定义了一个方法 loadUserByUsername(String username)用于完成用户信息的查 询,其中username就是登录时的登录名称,登录认证时,需要自定义一个实现类实现UserDetailService接 口,完成数据库查询,该接口返回UserDetail。
????????UserDetail主要用于封装认证成功时的用户信息,即UserDetailService返回的用户信息,可以用Spring 自己的User对象,但是最好是实现UserDetail接口,自定义用户对象。
2.Spring Security认证步骤:
????????1. 自定UserDetails类:当实体对象字段不满足时需要自定义UserDetails,一般都要自定义 UserDetails。
????????2. 自定义UserDetailsService类,主要用于从数据库查询用户信息。
????????3. 创建登录认证成功处理器,认证成功后需要返回JSON数据,菜单权限等。
????????4. 创建登录认证失败处理器,认证失败需要返回JSON数据,给前端判断。
????????5. 创建匿名用户访问无权限资源时处理器,匿名用户访问时,需要提示JSON。
????????6. 创建认证过的用户访问无权限资源时的处理器,无权限访问时,需要提示JSON。
????????7. 配置Spring Security配置类,把上面自定义的处理器交给Spring Security。
3.Spring Security认证实现:
????????3.3.1 添加Spring Security依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
????????3.3.2 自定义UserDetails类:
????????当实体对象字段不满足时Spring Security认证时,需要自定义UserDetails。
? ? ? ? 3.3.3 注意点:
? ? ? ? ????????3.3.3.1. 将User类实现UserDetails接口
? ? ? ? ????????3.3.3.2. 将原有的isAccountNonExpired、isAccountNonLocked,isCredentialsNonExpired和isEnabled属性修 改成boolean类型,同时添加authorities属性。你可以点击userdetail去看看,里面都是boolean类型,不是Boolean类型的。
注意:这五个是集成spring security必须要用到的UserDetails类几个属性.
@Data
@TableName("sys_user")
public class User implements Serializable, UserDetails {
//省略原有的属性......
/**
* 帐户是否过期(1 未过期,0已过期)
*/
private boolean isAccountNonExpired = true;
/**
* 帐户是否被锁定(1 未过期,0已过期)
*/
private boolean isAccountNonLocked = true;
/**
* 密码是否过期(1 未过期,0已过期)
*/
private boolean isCredentialsNonExpired = true;
/**
* 帐户是否可用(1 可用,0 删除用户)
*/
private boolean isEnabled = true;
/**
* 权限列表
*/
@TableField(exist = false)
Collection<? extends GrantedAuthority> authorities;
}
????????实际项目开发的完整user实体类:
@TableName(value ="sys_user")
@Data
public class User implements Serializable , UserDetails {
/**
* 用户编号
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 登录名称(用户名)
*/
private String username;
/**
* 登录密码
*/
private String password;
/**
* 帐户是否过期(1-未过期,0-已过期)
*/
private boolean isAccountNonExpired = true;
/**
* 帐户是否被锁定(1-未过期,0-已过期)
*/
private boolean isAccountNonLocked = true;
/**
* 密码是否过期(1-未过期,0-已过期)
*/
private boolean isCredentialsNonExpired =true;
/**
* 帐户是否可用(1-可用,0-禁用)
*/
private boolean isEnabled =true;
/**
* 真实姓名
*/
private String realName;
/**
* 昵称
*/
private String nickName;
/**
* 所属部门ID
*/
private Long departmentId;
/**
* 所属部门名称
*/
private String departmentName;
/**
* 性别(0-男,1-女)
*/
private Integer gender;
/**
* 电话
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 用户头像
*/
private String avatar;
/**
* 是否是管理员(1-管理员)
*/
private Integer isAdmin;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date createTime;
/**
* 修改时间
*/
@TableField(fill = FieldFill.UPDATE)
private Date updateTime;
/**
* 是否删除(0-未删除,1-已删除)
*/
private Integer isDelete;
/**
* 权限列表
*/
@TableField(exist = false)
Collection<? extends GrantedAuthority> authorities;
/**
* 查询用户权限列表
*/
@TableField(exist = false)
private List<Permission> permissionList;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
User other = (User) that;
return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
&& (this.getUsername() == null ? other.getUsername() == null : this.getUsername().equals(other.getUsername()))
&& (this.getPassword() == null ? other.getPassword() == null : this.getPassword().equals(other.getPassword()))
&& (this.getRealName() == null ? other.getRealName() == null : this.getRealName().equals(other.getRealName()))
&& (this.getNickName() == null ? other.getNickName() == null : this.getNickName().equals(other.getNickName()))
&& (this.getDepartmentId() == null ? other.getDepartmentId() == null : this.getDepartmentId().equals(other.getDepartmentId()))
&& (this.getDepartmentName() == null ? other.getDepartmentName() == null : this.getDepartmentName().equals(other.getDepartmentName()))
&& (this.getGender() == null ? other.getGender() == null : this.getGender().equals(other.getGender()))
&& (this.getPhone() == null ? other.getPhone() == null : this.getPhone().equals(other.getPhone()))
&& (this.getEmail() == null ? other.getEmail() == null : this.getEmail().equals(other.getEmail()))
&& (this.getAvatar() == null ? other.getAvatar() == null : this.getAvatar().equals(other.getAvatar()))
&& (this.getIsAdmin() == null ? other.getIsAdmin() == null : this.getIsAdmin().equals(other.getIsAdmin()))
&& (this.getCreateTime() == null ? other.getCreateTime() == null : this.getCreateTime().equals(other.getCreateTime()))
&& (this.getUpdateTime() == null ? other.getUpdateTime() == null : this.getUpdateTime().equals(other.getUpdateTime()))
&& (this.getIsDelete() == null ? other.getIsDelete() == null : this.getIsDelete().equals(other.getIsDelete()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
result = prime * result + ((getUsername() == null) ? 0 : getUsername().hashCode());
result = prime * result + ((getPassword() == null) ? 0 : getPassword().hashCode());
result = prime * result + ((getRealName() == null) ? 0 : getRealName().hashCode());
result = prime * result + ((getNickName() == null) ? 0 : getNickName().hashCode());
result = prime * result + ((getDepartmentId() == null) ? 0 : getDepartmentId().hashCode());
result = prime * result + ((getDepartmentName() == null) ? 0 : getDepartmentName().hashCode());
result = prime * result + ((getGender() == null) ? 0 : getGender().hashCode());
result = prime * result + ((getPhone() == null) ? 0 : getPhone().hashCode());
result = prime * result + ((getEmail() == null) ? 0 : getEmail().hashCode());
result = prime * result + ((getAvatar() == null) ? 0 : getAvatar().hashCode());
result = prime * result + ((getIsAdmin() == null) ? 0 : getIsAdmin().hashCode());
result = prime * result + ((getCreateTime() == null) ? 0 : getCreateTime().hashCode());
result = prime * result + ((getUpdateTime() == null) ? 0 : getUpdateTime().hashCode());
result = prime * result + ((getIsDelete() == null) ? 0 : getIsDelete().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", username=").append(username);
sb.append(", password=").append(password);
sb.append(", isAccountNonExpired=").append(isAccountNonExpired);
sb.append(", isAccountNonLocked=").append(isAccountNonLocked);
sb.append(", isCredentialsNonExpired=").append(isCredentialsNonExpired);
sb.append(", isEnabled=").append(isEnabled);
sb.append(", realName=").append(realName);
sb.append(", nickName=").append(nickName);
sb.append(", departmentId=").append(departmentId);
sb.append(", departmentName=").append(departmentName);
sb.append(", gender=").append(gender);
sb.append(", phone=").append(phone);
sb.append(", email=").append(email);
sb.append(", avatar=").append(avatar);
sb.append(", isAdmin=").append(isAdmin);
sb.append(", createTime=").append(createTime);
sb.append(", updateTime=").append(updateTime);
sb.append(", isDelete=").append(isDelete);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
? ? 3.3.4:编写UserService接口? ? ?编写 根据用户名查询用户信息 的方法。
public interface UserService extends IService<User> {
/**
* 根据用户名查询用户信息
* @param userName
* @return
*/
User findUserByUserName(String userName);
}
3.3.5:编写UserService接口实现类?
@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements
UserService {
/**
* 根据用户名查询用户信息
*
* @param userName
* @return
*/
@Override
public User findUserByUserName(String userName) {
//创建条件构造器
QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
//用户名
queryWrapper.eq("username",userName);
//返回查询记录
return baseMapper.selectOne(queryWrapper);
}
}
3.3.6?自定义UserDetailsService类
????????创建CustomerUserDetailsService用户认证处理类,该类需要实 现UserDetailsService接口。
@Component
public class CustomerUserDetailsService implements UserDetailsService {
@Resource
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
//调用根据用户名查询用户信息的方法
User user = userService.findUserByUserName(username);
//如果对象为空,则认证失败
if (user == null) {
throw new UsernameNotFoundException("用户名或密码错误!");
}
return user;
}
}
3.3.7:编写自定义认证成功处理器
? ? ? ? 要实现一个接口AuthenticationSuccessHandler
/**
* 登录认证成功处理器类
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws IOException,
ServletException {
//设置客户端的响应的内容类型
response.setContentType("application/json;charset=UTF-8");
//获取当登录用户信息
User user = (User) authentication.getPrincipal();
//消除循环引用
String result = JSON.toJSONString(user,
SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
3.3.8:编写自定义认证失败处理器
? ? ? ? 实现一个接口AuthenticationFailureHandler
/**
* 用户认证失败处理类
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception) throws
IOException, ServletException {
//设置客户端响应编码格式
response.setContentType("application/json;charset=UTF-8");
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
String message = null;//提示信息
int code = 500;//错误编码
//判断异常类型
if(exception instanceof AccountExpiredException){
message = "账户过期,登录失败!";
}else if(exception instanceof BadCredentialsException){
message = "用户名或密码错误,登录失败!";
}else if(exception instanceof CredentialsExpiredException){
message = "密码过期,登录失败!";
}else if(exception instanceof DisabledException){
message = "账户被禁用,登录失败!";
}else if(exception instanceof LockedException){
message = "账户被锁,登录失败!";
}else if(exception instanceof InternalAuthenticationServiceException){
message = "账户不存在,登录失败!";
}else{
message = "登录失败!";
}
//将错误信息转换成JSON
String result =
JSON.toJSONString(Result.error().code(code).message(message));
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
3.3.9:编写认证用户无权限访问处理器
? ? ? ? 实现接口AccessDeniedHandler
/**
* 认证用户访问无权限资源时处理器
*/
@Component
public class CustomerAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException
{
//设置客户端的响应的内容类型
response.setContentType("application/json;charset=UTF-8");
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
//消除循环引用
String result = JSON.toJSONString(Result.error().code(700).message("无权限
访问,请联系管理员!"), SerializerFeature.DisableCircularReferenceDetect);
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
3.3.10:编写匿名用户访问资源处理器
? ? ? ? 实现接口AuthenticationEntryPoint
@Component
public class AnonymousAuthenticationHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse
response, AuthenticationException authException) throws IOException,
ServletException {
//设置客户端的响应的内容类型
response.setContentType("application/json;charset=UTF-8");
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
//消除循环引用
String result = JSON.toJSONString(Result.error().code(600).message("匿名用
户无权限访问!"), SerializerFeature.DisableCircularReferenceDetect);
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
3.3.11:编写Spring Security配置类
? ? ? ? 需要继承WebSecurityConfigurerAdapter
????????@EnableWebSecurity:开启springsecurity
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private CustomerUserDetailsService customerUserDetailsService;
@Resource
private LoginSuccessHandler loginSuccessHandler;
@Resource
private LoginFailureHandler loginFailureHandler;
@Resource
private AnonymousAuthenticationHandler anonymousAuthenticationHandler;
@Resource
private CustomerAccessDeniedHandler customerAccessDeniedHandler;
/**
* 注入加密处理类
*
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//登录前进行过滤
http.formLogin()
.loginProcessingUrl("/api/user/login")
// 设置登录验证成功或失败后的的跳转地址
.successHandler(loginSuccessHandler).failureHandler(loginFailureHandler)
// 禁用csrf防御机制
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//关闭session
.and()
.authorizeRequests()//设置拦截请求
.antMatchers("/api/user/login").permitAll()//登录请求放行
.anyRequest().authenticated()//其他请求都要拦截
.and()
.exceptionHandling()
.authenticationEntryPoint(anonymousAuthenticationHandler)//匿名用户无权限访问
.accessDeniedHandler(customerAccessDeniedHandler)//认证用户无权限访问
.and().cors();//开启跨域配置
}
/**
* 配置认证处理器
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(customerUserDetailsService).passwordEncoder(passwordEnco
der());
}
}
3.3.11 测试登录认证接口
3.4 认证成功返回token
????????3.4.3 认证成功处理器返回token信息
package com.manong.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginResult {
//用户编号
private Long id;
//状态码
private int code;
//token令牌
private String token;
//token过期时间
private Long expireTime;
}
????????编写token工具类:
@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtUtils {
//密钥
private String secret;
// 过期时间 毫秒
private Long expiration;
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return
Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(Signatur
eAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims =
Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
*
* @param userDetails 用户
* @return 令牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put(Claims.SUBJECT, userDetails.getUsername());
claims.put(Claims.ISSUED_AT, new Date());
return generateToken(claims);
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(Claims.ISSUED_AT, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
*
* @param token 令牌
* @param userDetails 用户
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
User user = (User) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
(3)编写全局配置文件
????????????????在application.properties全局配置文件中自定义jwt属性。
#jwt配置
#密钥
jwt.secret=com.manong
#过期时间
jwt.expiration=1800000
(4)认证成功处理器类返回token数据 在原有的LoginSuccessHandler登录认证成功处理器类上加入jwt相关代码。 注意:在原有的基础上添加
/**
* 登录认证成功处理器类
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws IOException,
ServletException {
//设置客户端的响应的内容类型
response.setContentType("application/json;charset=UTF-8");
//获取当登录用户信息
User user = (User) authentication.getPrincipal();
//生成token
String token = jwtUtils.generateToken(user);
//设置token签名密钥及过期时间
long expireTime = Jwts.parser() //获取DefaultJwtParser对象
.setSigningKey(jwtUtils.getSecret()) //设置签名的密钥
.parseClaimsJws(token.replace("jwt_", ""))
.getBody().getExpiration().getTime();//获取token过期时间
//创建登录结果对象
LoginResult loginResult = new LoginResult(user.getId(),
ResultCode.SUCCESS,token,expireTime);
//消除循环引用
String result = JSON.toJSONString(loginResult,
SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
3.6 token验证
????????3.6.1 添加Redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.6.2 编写全局配置文件
################################# Redis相关配置 #################################
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=10000
#自定义属性
spring.redis.expire=60000
3.6.3 编写Redis配置类
????????
/**
* redis配置类
*/
@Configuration
public class RedisConfig {
//缓存过期时间
@Value("${spring.redis.expire}")
private Long expire;
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory
factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String,
Object>();
// 配置连接工厂
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
//设置在生成 json 串时,对象中的成员的可见性
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//存储到redis的json将是有类型的数据
//指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等
会跑出异常
om.activateDefaultTyping(om.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new
StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
//@Cacheable注解字符集编码配置
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig();
config.entryTtl(Duration.ofMinutes(expire))//缓存过期时间
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(Red
isSerializer.string()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(R
edisSerializer.json()));
return RedisCacheManager
.builder(factory)
.cacheDefaults(config)
.build();
}
}
(2)编写RedisService业务工具类
????????
@Component
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
//存缓存
public void set(String key ,String value,Long timeOut){
redisTemplate.opsForValue().set(key,value,timeOut, TimeUnit.SECONDS);
}
//取缓存
public String get(String key){
return (String) redisTemplate.opsForValue().get(key);
}
//清除缓存
public void del(String key){
redisTemplate.delete(key);
}
}
3.6.4 设置登录认证请求地址 在application.properties全局配置文件中自定义登录验证的请求地址。
#登录请求地址(自定义)
request.login.url=/api/user/login
3.6.5 编写token验证过滤器类
/**
* token验证过滤器
*/
@Data
@Component
public class CheckTokenFilter extends OncePerRequestFilter {
@Resource
private JwtUtils jwtUtils;
@Resource
private CustomerUserDetailsService customerUserDetailsService;
@Resource
private LoginFailureHandler loginFailureHandler;
@Resource
private RedisService redisService;
//获取登录请求地址
@Value("${request.login.url}")
private String loginUrl;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) throws ServletException,
IOException {
try {
//获取当前请求的url地址
String url = request.getRequestURI();
//如果当前请求不是登录请求,则需要进行token验证
if (!url.equals(loginUrl)) {
this.validateToken(request);
}
} catch (AuthenticationException e) {
loginFailureHandler.onAuthenticationFailure(request, response, e);
}
//登录请求不需要验证token
doFilter(request, response, filterChain);
}
/**
* 验证token
*
* @param request
*/
private void validateToken(HttpServletRequest request) throws
AuthenticationException {
//从头部获取token信息
String token = request.getHeader("token");
//如果请求头部没有获取到token,则从请求的参数中进行获取
if (ObjectUtils.isEmpty(token)) {
token = request.getParameter("token");
}
//如果请求参数中也不存在token信息,则抛出异常
if (ObjectUtils.isEmpty(token)) {
throw new CustomerAuthenticationException("token不存在");
}
//判断redis中是否存在该token
String tokenKey = "token_" + token;
String redisToken = redisService.get(tokenKey);
//如果redis里面没有token,说明该token失效
if (ObjectUtils.isEmpty(redisToken)) {
throw new CustomerAuthenticationException("token已过期");
}
//如果token和Redis中的token不一致,则验证失败
if (!token.equals(redisToken)) {
throw new CustomerAuthenticationException("token验证失败");
}
//如果存在token,则从token中解析出用户名
String username = jwtUtils.getUsernameFromToken(token);
//如果用户名为空,则解析失败
if (ObjectUtils.isEmpty(username)) {
throw new CustomerAuthenticationException("token解析失败");
}
//获取用户信息
UserDetails userDetails =
customerUserDetailsService.loadUserByUsername(username);
//判断用户信息是否为空
if (userDetails == null) {
throw new CustomerAuthenticationException("token验证失败");
}
//创建身份验证对象
UsernamePasswordAuthenticationToken authenticationToken = new
UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authenticationToken.setDetails(new
WebAuthenticationDetailsSource().buildDetails(request));
//设置到Spring Security上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
3.6.6 配置token验证过滤器类
将CheckTokenFilter过滤器类交给Spring Security进行管理,在SpringSecurityConfig配置类中添加如下代 码。
@Resource
private CheckTokenFilter checkTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
//登录前进行过滤
http.addFilterBefore(checkTokenFilter,
UsernamePasswordAuthenticationFilter.class);
http.formLogin()
//省略后续代码....
}
3.6.7 编写自定义异常类
package com.manong.config.security.exception;
import org.springframework.security.core.AuthenticationException;
/**
* 自定义验证异常类
*/
public class CustomerAuthenticationException extends AuthenticationException {
public CustomerAuthenticationException(String message){
super(message);
}
}
3.6.8 token验证失败处理 在LoginFailureHandler用户认证失败处理类中加入判断。
/**
* 用户认证失败处理类
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws
IOException, ServletException {
//设置客户端响应编码格式
response.setContentType("application/json;charset=UTF-8");
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
String message = null;//提示信息
int code = 500;//错误编码
//判断异常类型
if(exception instanceof AccountExpiredException){
message = "账户过期,登录失败!";
}else if(exception instanceof BadCredentialsException){
message = "用户名或密码错误,登录失败!";
}else if(exception instanceof CredentialsExpiredException){
message = "密码过期,登录失败!";
}else if(exception instanceof DisabledException){
message = "账户被禁用,登录失败!";
}else if(exception instanceof LockedException){
message = "账户被锁,登录失败!";
}else if(exception instanceof InternalAuthenticationServiceException){
message = "账户不存在,登录失败!";
}else if(exception instanceof CustomerAuthenticationException){
message = exception.getMessage();
code = 600;
}else{
message = "登录失败!";
}
//将错误信息转换成JSON
String result =
JSON.toJSONString(Result.error().code(code).message(message));
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
3.6.9 认证成功处理 修改LoginSuccessHandler登录认证成功处理类,将token保存到Redis缓存中。
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private JwtUtils jwtUtils;
@Resource
private RedisService redisService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws IOException,
ServletException {
//省略原有代码......
//把生成的token存到redis
String tokenKey = "token_"+token;
redisService.set(tokenKey,token,jwtUtils.getExpiration() / 1000);
}
}
3.7 刷新token信息 3.7.1 创建TokenVo类 在com.manong.vo包下创建TokenVo类,该类用于保存Token信息。
package com.manong.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TokenVo {
//过期时间
private Long expireTime;
//token
private String token;
}
3.7.2 编写刷新token方法 在com.manong.controller包下创建SysUserController控制器类,并在该类中编写refreshToken刷新token的 方法。
@RestController
@RequestMapping("/api/sysUser")
public class SysUserController {
@Resource
private RedisService redisService;
@Resource
private JwtUtils jwtUtils;
/**
* 刷新token
*
* @param request
* @return
*/
@PostMapping("/refreshToken")
public Result refreshToken(HttpServletRequest request) {
//从header中获取前端提交的token
String token = request.getHeader("token");
//如果header中没有token,则从参数中获取
if (ObjectUtils.isEmpty(token)) {
token = request.getParameter("token");
}
//从Spring Security上下文获取用户信息
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
//获取身份信息
UserDetails details = (UserDetails) authentication.getPrincipal();
//重新生成token
String reToken = "";
//验证原来的token是否合法
if (jwtUtils.validateToken(token, details)) {
//生成新的token
reToken = jwtUtils.refreshToken(token);
}
//获取本次token的到期时间,交给前端做判断
long expireTime = Jwts.parser().setSigningKey(jwtUtils.getSecret())
.parseClaimsJws(reToken.replace("jwt_", ""))
.getBody().getExpiration().getTime();
//清除原来的token信息
String oldTokenKey = "token_" + token;
redisService.del(oldTokenKey);
//存储新的token
String newTokenKey = "token_" + reToken;
redisService.set(newTokenKey, reToken, jwtUtils.getExpiration() / 1000);
//创建TokenVo对象
TokenVo tokenVo = new TokenVo(expireTime, reToken);
//返回数据
return Result.ok(tokenVo).message("token生成成功");
}
}
3.7.3 接口运行测试 1. 先运行用户登录请求,生成token信息 2. 测试查询全部用户信息,预期结果是查询成功 3. 运行刷新token接口,重新生成token信息
|