目录
Spring Security认证步骤
- 自定UserDetails类:当实体对象字段不满足时需要自定义UserDetails,一般都要自定义
UserDetails。 - 自定义UserDetailsService类,主要用于从数据库查询用户信息。
- 创建登录认证成功处理器,认证成功后需要返回JSON数据,菜单权限等。
- 创建登录认证失败处理器,认证失败需要返回JSON数据,给前端判断。
- 创建匿名用户访问无权限资源时处理器,匿名用户访问时,需要提示JSON。
- 创建认证过的用户访问无权限资源时的处理器,无权限访问时,需要提示JSON。
- 配置Spring Security配置类,把上面自定义的处理器交给Spring Security。
Spring Security认证实现
添加Spring Security依赖
在pom.xml文件中添加Spring Security核心依赖,代码如下所示:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
自定义UserDetails类
当实体对象字段不满足时Spring Security认证时,需要自定义UserDetails。
- 将User类实现UserDetails接口
- 将原有的isAccountNonExpired、isAccountNonLocked、isCredentialsNonExpired和isEnabled属性修
改成boolean类型,同时添加authorities属性。
注意:上述4个属性只能是非包装类的boolean类型属性,且默认值设置为true。
@TableName(value = "user")
@Data
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)
private static final long serialVersionUID = 1L;
/**
* 权限列表
*/
@TableField(exist = false)
Collection<? extends GrantedAuthority> authorities;
/**
* 查询用户权限列表
*/
@TableField(exist = false)
private List<Permission> permissionList;
}
编写UserService接口
在 com.example.mybox.service.UserService
接口编写 根据用户名查询用户信息
的方法
/**
* @author Mr.Li
* @description 针对表【user】的数据库操作Service
* @createDate 2024-09-25 14:21:20
*/
public interface UserService extends IService<User> {
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
User findUserByUserName(String username);
}
编写UserService接口实现类
在 com.example.mybox.service.impl.UserServiceImpl
类中实现 UserService
接口。
/**
* @author Mr.Li
* @description 针对表【user】的数据库操作Service实现
* @createDate 2024-09-25 14:21:20
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
/**
* 根据用户名查询用户信息
*
* @param username
* @return
*/
@Override
public User findUserByUserName(String username) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
return baseMapper.selectOne(queryWrapper);
}
}
自定义UserDetailsService类
在 com.example.mybox.config.security.service
包下创建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);
//根据user是否存在判断认证情况
if (user == null) {
throw new UsernameNotFoundException("用户名或密码错误");
}
return user;
}
}
编写自定义认证成功处理器
在 com.example.mybox.config.security.handler
包下创建 LoginSuccessHandler
登录认证成功处理器类。
/**
* 登录认证成功处理器类
*/
@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 {
//设置客户端的响应的内容类型
response.setContentType("application/json;charset=UTF-8");
//获取当登录用户信息
User user = (User) authentication.getPrincipal();
//消除循环引用
String result = JSON.toJSONString(loginResult, SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
编写自定义认证失败处理器
在 com.example.mybox.config.security.handler
包下创建 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 = ResultCode.UNAUTHORIZED_CODE;//错误编码
//判断异常类型
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();
} else {
message = "登录失败!";
}
//将错误信息转换成JSON
String result = JSON.toJSONString(Result.error(message, code));
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
编写认证用户无权限访问处理器
在 com.example.mybox.config.security.handler
包下创建 CustomerAccessDeniedHandler
认证用户访问无权限资源时处理器类。
/**
* 访问无权限处理器
*/
@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("无权限,请联系管理员!", ResultCode.NOT_ALLOWED_CODE),
SerializerFeature.DisableCircularReferenceDetect);
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
编写匿名用户访问资源处理器
在 com.example.mybox.config.security.handler
包下创建 AnonymousAuthenticationHandler
匿名用户访问资源处理器类。
/**
* 匿名访问资源处理器
*/
@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("匿名无权限,请联系管理员!", ResultCode.NOT_ALLOWED_CODE),
SerializerFeature.DisableCircularReferenceDetect);
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
编写Spring Security配置类
在 com.example.mybox.config.security
包下创建 SpringSecurityConfig
配置类。
@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();
}
/**
* 处理登录认证
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginProcessingUrl("/user/login")
// 设置登录验证成功或失败后的的跳转地址
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
// 禁用csrf防御机制
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/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(passwordEncoder());
}
}
测试登录认证接口
认证成功返回token
什么是token?
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
token认证流程图
认证成功处理器返回token信息
(1)封装token返回的数据信息
/**
* 封装token返回的数据信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginResult {
//用户名
private String username;
//状态码
private int code;
//token令牌
private String token;
//token过期时间
private Long expireTime;
}
(2)编写token工具类
@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtUtils {
//密钥
@Value("${jwt.secret}")
private String secret;
// 过期时间 毫秒
@Value("${jwt.expiration}")
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(SignatureAlgorithm.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.yml
全局配置文件中自定义jwt属性。
# jwt 配置
jwt:
# 有效期1天(单位:s)
expiration: 1800000
# secret: 秘钥(普通字符串)
secret: aHR0cHM6Ly9teS5vc2NoaW5hLm5ldC91LzM2ODE4Njg=
(4)认证成功处理器类返回token数据
在原有的 LoginSuccessHandler
登录认证成功处理器类上加入jwt相关代码。
/**
* 登录认证成功处理器类
*/
@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 {
//设置客户端的响应的内容类型
response.setContentType("application/json;charset=UTF-8");
//获取当登录用户信息
User user = (User) authentication.getPrincipal();
//生成token
String token = jwtUtils.generateToken(user);
//设置token签名以及过期时间
long jwt = Jwts.parser()
.setSigningKey(jwtUtils.getSecret())
.parseClaimsJws(token.replace("jwt_", ""))
.getBody().getExpiration().getTime();
LoginResult loginResult = new LoginResult(user.getUsername(), ResultCode.SUCCESS_CODE, token, jwt);
//消除循环引用
String result = JSON.toJSONString(loginResult, SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
//把生成的token存到redis
String tokenKey = "token_"+token;
redisService.set(tokenKey,token,jwtUtils.getExpiration() / 1000);
}
}
最终效果