本文将以代码示例介绍基于SpringSecurity结合JWT和Redis实现用户的登录和登出。
- 如文章中有明显错误或者用词不当的地方,欢迎大家在评论区批评指正,我看到后会及时修改。
- 如想要和博主进行技术栈方面的讨论和交流可私信我。
目录
3.2. 重写UsernamePasswordAuthenticationFilter过滤器
3.4. 重写AuthenticationSuccessHandler处理用户登录成功操作
3.5. 重写AuthenticationFailureHandler处理用户登录失败操作
3.9. 重写LogoutSuccessHandler处理用户退出成功操作
1. Spring Security 介绍
Spring Security 是基于 Spring 的身份认证(Authentication)和用户授权(Authorization)框架,提供了一 套 Web 应用安全性的完整解决方案。其中核心技术使用了 Servlet 过滤器、IOC 和 AOP 等
1.1. 什么是身份认证
身份认证指的是用户去访问系统资源时,系统要求验证用户的身份信息,用户身份合法才访问对应资源。 常见的身份认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
1.2. 什么是用户授权
当身份认证通过后,去访问系统的资源,系统会判断用户是否拥有访问该资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。 比如 会员管理模块有增删改查功能,有的用户只能进行查询,而有的用户可以进行修改、删除。一般来说, 系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
2. 开发环境搭建
2.1. 所用工具版本
依赖 | 版本 |
---|---|
Spring Boot | 2.6.14 |
java | 1.8 |
redis | 6.2 |
2.2. pom依赖
1. 引入SpringBoot依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.14</version> <relativePath/> <!-- lookup parent from repository --> </parent>
2. 引入Spring Security依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
3. 引入redis依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.6.14</version> </dependency>
4. 引入JWT依赖
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
3. 核心代码编写
3.1. 编写Security授权配置主文件
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("authUserDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Autowired
private SecurOncePerRequestFilter securOncePerRequestFilter;
@Autowired
private SecurAuthenticationEntryPoint securAuthenticationEntryPoint;
@Autowired
private SecurAccessDeniedHandler securAccessDeniedHandler;
//登录成功处理器
@Autowired
private SecurAuthenticationSuccessHandler securAuthenticationSuccessHandler;
@Autowired
private SecurAuthenticationFailureHandler securAuthenticationFailureHandler;
//退出处理器
@Autowired
private SecurLogoutHandler securLogoutHandler;
@Autowired
private SecurLogoutSuccessHandler securLogoutSuccessHandler;
@Autowired
BCryptPasswordEncoderUtil bCryptPasswordEncoderUtil;
/**
* 从容器中取出 AuthenticationManagerBuilder,执行方法里面的逻辑之后,放回容器
*
* @param authenticationManagerBuilder
* @throws Exception
*/
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoderUtil);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//第1步:解决跨域问题。cors 预检请求放行,让Spring security 放行所有preflight request(cors 预检请求)
http.authorizeRequests().requestMatchers(CorsUtils::isPreFlightRequest).permitAll();
//第2步:让Security永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContext
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().headers().cacheControl();
//第3步:请求权限配置
//放行注册API请求,其它任何请求都必须经过身份验证.
http.authorizeRequests()
.antMatchers(HttpMethod.POST,"/sys-user/register").permitAll()
.antMatchers("/v2/api-docs", "/v2/feign-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources","/swagger-resources/configuration/security",
"/swagger-ui.html", "/webjars/**").permitAll()
.anyRequest().authenticated();
//第4步:拦截账号、密码。覆盖 UsernamePasswordAuthenticationFilter过滤器
http.addFilterAt(securUsernamePasswordAuthenticationFilter() , UsernamePasswordAuthenticationFilter.class);
//第5步:拦截token,并检测。在 UsernamePasswordAuthenticationFilter 之前添加 JwtAuthenticationTokenFilter
http.addFilterBefore(securOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
//第6步:处理异常情况:认证失败和权限不足
http.exceptionHandling().authenticationEntryPoint(securAuthenticationEntryPoint).accessDeniedHandler(securAccessDeniedHandler);
//第7步:登录,因为使用前端发送JSON方式进行登录,所以登录模式不设置也是可以的。
http.formLogin();
//第8步:退出
http.logout().addLogoutHandler(securLogoutHandler).logoutSuccessHandler(securLogoutSuccessHandler);
}
/**
* 手动注册账号、密码拦截器
* @return
* @throws Exception
*/
@Bean
SecurUsernamePasswordAuthenticationFilter securUsernamePasswordAuthenticationFilter() throws Exception {
SecurUsernamePasswordAuthenticationFilter filter = new SecurUsernamePasswordAuthenticationFilter();
//成功后处理
filter.setAuthenticationSuccessHandler(securAuthenticationSuccessHandler);
//失败后处理
filter.setAuthenticationFailureHandler(securAuthenticationFailureHandler);
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
}
3.2. 重写UsernamePasswordAuthenticationFilter过滤器
public class SecurUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
ISysUserService userService;
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
|| request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest = null;
//取authenticationBean
Map<String, String> authenticationBean = null;
//用try with resource,方便自动释放资源
try (InputStream is = request.getInputStream()) {
authenticationBean = mapper.readValue(is, Map.class);
} catch (IOException e) {
//将异常放到自定义的异常类中
throw new SecurAuthenticationException(e.getMessage());
}
try {
if (!authenticationBean.isEmpty()) {
//获得账号、密码
String username = authenticationBean.get(SPRING_SECURITY_FORM_USERNAME_KEY);
String password = authenticationBean.get(SPRING_SECURITY_FORM_PASSWORD_KEY);
//检测账号、密码是否存在
if (userService.checkLogin(username, password)) {
//将账号、密码装入UsernamePasswordAuthenticationToken中
authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request,authRequest );
return this.getAuthenticationManager().authenticate(authRequest);
}
}
} catch (Exception e) {
throw new SecurAuthenticationException(e.getMessage());
}
return null;
} else {
return this.attemptAuthentication(request, response);
}
}
}
3.3. 编写拦截器拦截请求token
@Component
public class SecurOncePerRequestFilter extends OncePerRequestFilter {
@Qualifier("authUserDetailsServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
private String header = "Authorization";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String headerToken = request.getHeader(header);
if (!StringUtils.isEmpty(headerToken)) {
String token = headerToken.replace("Bearer", "").trim();
boolean check = false;
try {
check = this.jwtTokenUtil.isTokenExpired(token);
} catch (Exception e) {
new Throwable("令牌已过期,请重新登录。"+e.getMessage());
}
if (!check){
//通过令牌获取用户名称
String username = jwtTokenUtil.getUsernameFromToken(token);
//判断用户不为空,且SecurityContextHolder授权信息还是空的
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//通过用户信息得到UserDetails
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//验证令牌有效性
boolean validata = false;
try {
validata = jwtTokenUtil.validateToken(token, userDetails);
}catch (Exception e) {
new Throwable("验证token无效:"+e.getMessage());
}
if (validata) {
// 将用户信息存入 authentication,方便后续校验
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
//
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将 authentication 存入 ThreadLocal,方便后续获取用户信息
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
}
chain.doFilter(request, response);
}
}
3.4. 重写AuthenticationSuccessHandler处理用户登录成功操作
@Component
@Slf4j
public class SecurAuthenticationSuccessHandler extends JSONAuthentication implements AuthenticationSuccessHandler {
@Autowired
ISysUserService service;
@Autowired
RedisUtils redisUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//取得账号信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
//用户鉴权
SecurityContextHolder.getContext().setAuthentication(authentication);
String token=redisUtils.get(TOKEN_KEY+userDetails.getUsername())==null?"":redisUtils.get(TOKEN_KEY+userDetails.getUsername()).toString();
if(token =="") {
System.out.println("初次登录,token还没有,生成新token。。。。。。");
//如果token为空,则去创建一个新的token
jwtTokenUtil = new JwtTokenUtil();
token = jwtTokenUtil.generateToken(userDetails);
redisUtils.set(TOKEN_KEY+userDetails.getUsername(),token,3600L * 11);
}
redisUtils.sSetAndTime(VISIT_USER_KEY,60*60*24,userDetails.getUsername()+ System.currentTimeMillis());
//加载前端菜单
Map<String,Object> map = new HashMap<>();
List<UserMenuVo> menus = null;
try {
menus = service.getUserMenus(userDetails.getUsername());
} catch (Exception e) {
e.printStackTrace();
R<Map<String,Object>> data = R.failed("获取用户菜单失败");
this.WriteJSON(request, response, data);
return;
}
map.put("username",userDetails.getUsername());
map.put("auth",userDetails.getAuthorities());
map.put("menus",menus);
map.put("token",token);
//装入token
ResponseStructure data = ResponseStructure.success(map);
//输出
this.WriteJSON(request, response, data);
}
}
上述代码主要功能为完成认证并将token塞入redis,因为JWT设置token的过期时间是12个小时,在redis里面我提前了一点,设置了11个小时过期,最后还获取了用户对应菜单(这块之后文章会补充)一起返回给前端。
3.5. 重写AuthenticationFailureHandler处理用户登录失败操作
@Component
public class SecurAuthenticationFailureHandler extends JSONAuthentication implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
ResponseStructure data = ResponseStructure.instance(ALL_RETURN_401.getCode(),"登录失败:"+e.getMessage());
//输出
this.WriteJSON(request, response, data);
}
}
3.6. 身份校验失败处理器
@Component
public class SecurAuthenticationEntryPoint extends JSONAuthentication implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ResponseStructure data = ResponseStructure.instance(ALL_RETURN_401.getCode(),"token不可用或已过期");
this.WriteJSON(request, response, data);
}
}
3.7. 编写权限校验处理器
@Component
public class SecurAccessDeniedHandler extends JSONAuthentication implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseStructure data = ResponseStructure.failed("权限不足:"+accessDeniedException.getMessage());
this.WriteJSON(request, response, data);
}
}
3.8. 重写LogoutHandler处理用户退出操作
@Component
public class SecurLogoutHandler extends JSONAuthentication implements LogoutHandler {
private String header = "Authorization";
@Override
public void logout(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
String headerToken = request.getHeader(header);
if (!StringUtils.isEmpty(headerToken)) {
SecurityContextHolder.clearContext();
}
}
}
3.9. 重写LogoutSuccessHandler处理用户退出成功操作
@Component
public class SecurLogoutSuccessHandler extends JSONAuthentication implements LogoutSuccessHandler {
@Autowired
RedisUtils redisUtils;
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
ResponseStructure data = ResponseStructure.success("退出成功");
super.WriteJSON(request,response,data);
}
}
3.10 编写JWT工具类
/**
* JWT生成令牌、验证令牌、获取令牌
*/
@Component
public class JwtTokenUtil {
//私钥
private static final String SECRET_KEY = "coding";
// 过期时间 毫秒,设置默认12个小时过期
private static final long EXPIRATION_TIME = 3600000L * 12;
/**
* 生成令牌
*
* @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 = null;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
System.out.println("e = " + e.getMessage());
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) throws Exception {
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) throws Exception {
AuthUser user = (AuthUser) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET_KEY).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims getClaimsFromToken(String token) throws Exception {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
} catch (Exception e) {
new Throwable(e);
}
return claims;
}
}
4. 代码测试
4.1. 登录测试
使用postman测试登录接口/login
4.2. 退出测试
使用postman测试登录接口/logout