Spring Security介绍
Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。总之,Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。
使用版本:
spring-boot-starter-security 2.7.10springboot全家桶依赖 2.7.10
使用写法:
security 共两种写法 分别为mvc 与webflux
本系统时间有限采用mvc方式,经典且自由。
思路一:如果采用webflux 可以与网关gateway模块合并鉴权。
一、加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.10</version>
</dependency>
二、配置文件配置(只给出了权限部分的)
jwt:
token:
header: Authorization
expired: 10000 #token 过期时间 单位秒
secret: cereshuzhitingnizhenbangcereshuzhitingnizhenbang #token 密钥
refresh:
expired: 30 #token 刷新时间 单位秒
security:
noFilter:
- /swagger-ui/** # Swagger UI v3
- /swagger-resources/**
- /v2/api-docs/** # Swagger v2
- /auth/v2/api-docs
- /v2/api-docs # Swagger v2
- /**/v2/api-docs # Swagger v2
- /webjars/** # Webjars, typically for static resources
- /favicon.ico # Favicon
- /css/** # CSS files
- /js/** # JavaScript files
- /images/** # Images
- /public/** # Public folder
- /static/** # Static resources
- /doc.html
- /webjars/**
- /system/logout
- /login
- /permission/**
- /getCaptcha #获取验证码
- /system/sysUser/list
- /**/doc.html
三、配置读取
SecurityPathConfig 白名单配置读取JwtConfig jwt token配置读取
JwtConfig.java
import lombok.Data;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@ConfigurationProperties("jwt.token")
@Configuration
public class JwtConfig {
//token 密钥
private String secret;
//token 过期时间 单位秒
private Long expired;
//token 刷新时间 单位秒
@Value("${jwt.token.refresh.expired}")
private Long refreshExpired;
//header 请求鉴权头
private String header;
}
SecurityPathConfig.java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@ConfigurationProperties("security")
@Configuration
@Data
public class SecurityPathConfig {
List<String> noFilter;
}
四、封装User对象实体
LoginDto为获取用户实体 根据自己的用户实体进行放入
import cn.hutool.core.lang.Assert;
import com.pinyi.supply.system.dto.LoginDto;
import com.pinyi.supply.system.model.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class LoginUser implements UserDetails {
private LoginDto loginDto;
public LoginDto getLoginDto() {
return loginDto;
}
public void setLoginDto(LoginDto loginDto) {
this.loginDto = loginDto;
}
private static final long serialVersionUID = 540L;
private String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public LoginUser(LoginDto loginDto, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(loginDto, username, password, true, true, true, true, authorities);
}
public LoginUser(LoginDto loginDto, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
this.loginDto = loginDto;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
五、自定义获取用户
import com.pinyi.security.security.entity.LoginUser;
import com.pinyi.supply.system.dto.LoginDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("查询用户");
LoginDto userDto = sysUserService.getByUsername(username);
if (userDto == null) {
throw new UsernameNotFoundException("用户不存在");
}
return new LoginUser(userDto, userDto.getSysUser().getLoginName()
, userDto.getSysUser().getPassword(), getUserAuthority(userDto.getPermissions()));
}
/**
* 获取用户权限信息(角色、菜单权限)
*
* @param permissions
* @return
*/
public List<GrantedAuthority> getUserAuthority(List<String> permissions) {
// 实际怎么写以数据表结构为准,这里只是写个例子
// 角色(比如ROLE_admin),菜单操作权限(比如sys:user:list)
List<GrantedAuthority> listPram = new ArrayList<>();
for (String s : permissions) {
listPram.add(new SimpleGrantedAuthority(s));
}
return listPram;
}
}
六、查询用户service
remoteSystemService 采用的Feign 调用的用户服务获取信息。
@Service
public class SysUserService {
@Resource
private RemoteSystemService remoteSystemService;
public LoginDto getByUsername(String userName) {
LoginDto result = remoteSystemService.getUserByUsername(userName);
if (ObjectUtils.isEmpty(result)) {
throw new UsernameNotFoundException("用户不存在");
}
return result;
}
}
Feign 接口实例
import com.pinyi.security.security.feign.factory.RemoteAuthServiceFactory;
import com.pinyi.supply.common.http.HttpResult;
import com.pinyi.supply.system.dto.LoginDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(contextId = "systemService", value = "system", fallbackFactory = RemoteAuthServiceFactory.class)
public interface RemoteSystemService {
@GetMapping("/sysUser/getUserByUsername")
public LoginDto getUserByUsername(@RequestParam(value = "username") String username);
}
七、登录实现
@Api(tags = "用户授权接口")
@RestController
@RequestMapping
public class AuthController {
@Autowired
LoginService loginService;
@Autowired
JwtTokenUtils jwtUtils;
/**
* 用户登录接口
* 该方法处理用户登录请求,通过POST方法提交登录信息
*
* @param username 用户名,用于识别用户身份
* @param password 密码,用户账户的安全凭证
* @param captcha 验证码,用于防止暴力破解,提高登录安全性
*/
@ApiOperation(value = "登录接口", notes = "登录接口")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "成功", response = HttpResult.class),
@ApiResponse(code = 500, message = "服务器内部错误")
})
@PostMapping("/login")
public void login(@ApiParam(value = "账号", required = true) @RequestParam(value = "username") String username,
@ApiParam(value = "密码", required = true) @RequestParam(value = "password") String password,
@ApiParam(value = "验证码", required = true) @RequestParam(value = "captcha") String captcha) {
loginService.login(new LoginEntity(username, password, captcha));
}
/**
* 获取验证码接口
*
* @param request HTTP请求对象
* @param response HTTP响应对象
* @param username 用户名,用于识别请求验证码的用户
* @return 返回HttpResult对象,包含验证码信息
* <p>
* 说明:
* 1. 该方法通过调用loginService.addCaptcha(username)生成验证码,并将验证码与用户名关联后存储。
* 2. 成功生成验证码后,通过HttpResult对象返回验证码信息。
* 3. 该方法使用@GetMapping注解,指定请求URL为"/getCaptcha",处理GET类型的请求。
*/
@ApiOperation(value = "获取验证码", notes = "获取验证码")
@ApiImplicitParams({
@ApiImplicitParam(name = "username", value = "用户名", required = true, dataType = "String", paramType = "query")
})
@GetMapping("/getCaptcha")
public HttpResult getCaptcha(HttpServletRequest request, HttpServletResponse response, String username) {
String captcha = loginService.addCaptcha(username);
return HttpResult.success("获取成功", captcha);
}
}
登录service接口
import com.pinyi.security.security.entity.dto.LoginEntity;
import com.pinyi.supply.system.dto.LoginDto;
public interface LoginService {
/**
* 登录
* @param entity 登录对象
* @return token
*/
String login(LoginEntity entity);
/**
* 获取用户信息
* @return 用户信息
*/
LoginDto getUserInfo();
/**
* 退出登录
* @return 是否成功
*/
Boolean logout();
/**
* 获取验证码
* @param username 用户名
* @return 验证码
*/
String addCaptcha(String username);
}
实现service
import com.pinyi.security.security.entity.dto.LoginEntity;
import com.pinyi.supply.common.utils.StringUtils;
import com.pinyi.supply.redis.service.RedisService;
import com.pinyi.supply.system.dto.LoginDto;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import static com.pinyi.supply.redis.enuns.RedisCacheEnum.CAPTCHA_PATH;
@Service
public class LoginServiceImpl implements LoginService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisService redisService;
/**
* 登录方法
*
* @param entity 登录对象
* @return 登录结果
*/
@Override
public String login(LoginEntity entity) {
//进行用户认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(entity.getUsername(), entity.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//认证未通过,给出提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登陆失败!");
}
return "";
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@Override
public LoginDto getUserInfo() {
UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
//details里面可能存放了当前登录用户的详细信息,也可以通过cast后拿到
LoginDto loginDto = (LoginDto) authenticationToken.getPrincipal();
if (loginDto != null && loginDto.getSysUser() != null) {
loginDto.getSysUser().setPassword(null);
}
return loginDto;
}
@Override
public Boolean logout() {
return true;
}
/**
* 生成并返回一个随机的验证码
*
* @param username 用户名,此参数在本方法中未被使用
* @return 返回一个随机生成的验证码字符串
* <p>
* 方法说明:
* 1. 该方法的作用是添加并返回一个随机验证码,验证码与特定用户无关,因此不使用username参数
* 2. 验证码生成的具体逻辑在random3()方法中,此处应调用random3()以获取验证码
* 3. 由于方法功能简单明了,故无需额外导入类或复杂逻辑
*/
@Override
public String addCaptcha(String username) {
if (StringUtils.isBlank(username)) {
throw new RuntimeException("用户名不能为空");
}
String code = random3();
//redis存储
redisService.set(CAPTCHA_PATH.getPath() + username, code, 120);
return code;
}
/**
* 生成并返回一个随机的验证码
*
* @return 返回一个随机生成的验证码字符串
* <p>
* 方法说明:
* 1. 该方法的作用是添加并返回一个随机验证码,验证码与特定用户无关,因此不使用username参数
* 2. 验证码生成的具体逻辑在random3()方法中,此处应调用random3()以获取验证码
* 3. 由于方法功能简单明了,故无需额外导入类或复杂逻辑
*/
private static String random3() {
// jdk1.7出的随机生成数,关键还并发安全
// nextInt(int origin, int bound) 范围:[origin,bound)
return String.valueOf(ThreadLocalRandom.current().nextInt(100000, 1000000));
}
}
八、security配置config
由于配置了加密
BCryptPasswordEncoder 所以用户密码要存储该方式加密字符
import com.pinyi.security.security.filter.CaptchaFilter;
import com.pinyi.security.security.filter.JwtAuthenticationEntryPoint;
import com.pinyi.security.security.filter.JwtAuthenticationFilter;
import com.pinyi.security.security.handler.*;
import com.pinyi.security.security.service.UserDetailServiceImpl;
import com.pinyi.security.security.utils.JwtTokenUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
JWTLogoutSuccessHandler jwtLogoutSuccessHandler;
@Autowired
JwtCustomLogoutHandler jwtCustomLogoutHandler;
@Autowired
SecurityPathConfig securityPathConfig;
@Autowired
JwtTokenUtils jwtTokenUtils;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
public String[] getOpenPaths() {
return securityPathConfig.getNoFilter().toArray(new String[0]);
}
// @Bean
// PasswordEncoder PasswordEncoder() {
// return new PasswordEncoder();
// }
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
// 登录配置
.formLogin()
.loginProcessingUrl("/login")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.addLogoutHandler(jwtCustomLogoutHandler)
.permitAll()
.logoutSuccessUrl("/")
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(getOpenPaths()).permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
// 验证码过滤器放在UsernamePassword过滤器之前
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationFilter(), LogoutFilter.class)
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService);
}
}
九、拦截器处理
使用拦截器介绍
CaptchaFilter 验证码拦截器 (只登录时拦截)
import com.pinyi.security.security.exception.CaptchaException;
import com.pinyi.security.security.handler.LoginFailureHandler;
import com.pinyi.supply.redis.enuns.RedisCacheEnum;
import com.pinyi.supply.redis.service.RedisService;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@Component
@Log4j2
public class CaptchaFilter extends OncePerRequestFilter {
private static final String LOGIN_URL = "/login";
private static final String POST_METHOD = "POST";
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
private RedisService redisService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
log.debug("验证码拦截器处理");
String url = httpServletRequest.getRequestURI();
String method = httpServletRequest.getMethod();
if (Objects.equals(LOGIN_URL, url) && Objects.equals(POST_METHOD, method)) {
handleLoginValidation(httpServletRequest, httpServletResponse);
filterChain.doFilter(httpServletRequest, httpServletResponse);
} else {
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
// 处理登录验证码
private void handleLoginValidation(HttpServletRequest request, HttpServletResponse response) throws CaptchaException, ServletException, IOException {
try {
validate(request);
} catch (CaptchaException e) {
log.error("验证码校验失败: {}", e.getMessage(), e);
loginFailureHandler.onAuthenticationFailure(request, response, e);
}
}
// 校验验证码逻辑
private void validate(HttpServletRequest request) {
String code = request.getParameter("captcha");
String username = request.getParameter("username");
if (StringUtils.isBlank(username) || StringUtils.isBlank(code)) {
throw new CaptchaException("用户名与验证码不能为空");
}
if (redisService.hasKey(RedisCacheEnum.CAPTCHA_PATH.getPath() + username)) {
String captcha = (String) redisService.get(RedisCacheEnum.CAPTCHA_PATH.getPath() + username);
if (!captcha.equals(code)) {
throw new CaptchaException("验证码错误");
}
} else {
throw new CaptchaException("验证码已过期");
}
if (StringUtils.isBlank(code)) {
throw new CaptchaException("验证码错误");
}
}
}
JwtAuthenticationEntryPoint 认证失败过滤器
import com.alibaba.fastjson.JSON;
import com.pinyi.security.security.enums.AuthorizationCodeEnum;
import com.pinyi.supply.common.http.HttpResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
logger.info("JWT认证失败处理器JwtAuthenticationEntryPoint");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_OK); // 使用常量代替数字提高可读性
try (ServletOutputStream outputStream = httpServletResponse.getOutputStream()) {
String jsonResult = JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TEMPORARILY_WITHOUT_AUTHORITY)); // 使用Jackson转换对象为JSON字符串
outputStream.write(jsonResult.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch (IOException ex) {
logger.error("Failed to write response", ex);
}
}
}
JwtAuthenticationFilter 每次请求(jwt身份)认证过滤器
import com.alibaba.fastjson.JSON;
import com.pinyi.security.security.config.JwtConfig;
import com.pinyi.security.security.config.SecurityPathConfig;
import com.pinyi.security.security.enums.AuthorizationCodeEnum;
import com.pinyi.security.security.service.SysUserService;
import com.pinyi.security.security.service.UserDetailServiceImpl;
import com.pinyi.security.security.utils.JwtTokenUtils;
import com.pinyi.security.security.utils.WhitelistChecker;
import com.pinyi.supply.common.exception.BusinessRuntimeException;
import com.pinyi.supply.common.http.HttpResult;
import com.pinyi.supply.common.utils.StringUtils;
import com.pinyi.supply.redis.enuns.RedisCacheEnum;
import com.pinyi.supply.redis.service.RedisService;
import com.pinyi.supply.system.dto.LoginDto;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtTokenUtils jwtUtils;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
SysUserService sysUserService;
@Autowired
JwtConfig jwtConfig;
@Autowired
SecurityPathConfig securityPathConfig;
@Autowired
RedisService redisService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String url = request.getRequestURI();
System.out.println("JWT过滤器,url-" + url);
response.setCharacterEncoding("UTF-8");
String jwt = request.getHeader(jwtConfig.getHeader());
// 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的
// 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
//跳过不需要验证token
WhitelistChecker checker = new WhitelistChecker(securityPathConfig.getNoFilter());
if (checker.isWhitelisted(url)) {
chain.doFilter(request, response);
return;
}
//校验jwt
if (StringUtils.isBlank(jwt)) {
//token为空
response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_NULL)));
return;
}
try {
UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (ExpiredJwtException e) {
response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_EXPIRED)));
logger.error("Token已过期: {} " + e);
} catch (UnsupportedJwtException e) {
response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_FORMAT_ERROR)));
logger.error("Token格式错误: {} " + e);
} catch (MalformedJwtException e) {
response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_IS_NOT_CONSTRUCTED_CORRECTLY)));
logger.error("Token没有被正确构造: {} " + e);
} catch (IllegalArgumentException e) {
response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_INVALID_PARAMETER_IS_ABNORMAL)));
logger.error("非法参数异常: {} " + e);
} catch (Exception e) {
response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_LESS)));
logger.error("无效token" + e.getMessage());
}
}
//获取用户凭证
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) {
String token = request.getHeader(jwtConfig.getHeader());
if (token != null) {
String userName = "";
LoginDto loginDto = null;
try {
// 解密Token
userName = jwtUtils.getUserName(token);
if (StringUtils.isNotBlank(userName)) {
// 判断redis是否包含该用户信息
boolean hasKey = redisService.hasKey(RedisCacheEnum.LOGIN_PATH.getPath() + userName + RedisCacheEnum.USER_PATH.getPath());
if (!hasKey) {
throw new BusinessRuntimeException("登录已过期,请重新登录");
}
loginDto = JSON.parseObject(redisService.get(RedisCacheEnum.LOGIN_PATH.getPath() + userName + RedisCacheEnum.USER_PATH.getPath()).toString(), LoginDto.class);
// 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录
UsernamePasswordAuthenticationToken tokenAuth = new UsernamePasswordAuthenticationToken(loginDto, null, userDetailService.getUserAuthority(loginDto.getPermissions()));
return tokenAuth;
}
} catch (ExpiredJwtException e) {
throw e;
//throw new TokenException("Token已过期");
} catch (UnsupportedJwtException e) {
throw e;
//throw new TokenException("Token格式错误");
} catch (MalformedJwtException e) {
throw e;
//throw new TokenException("Token没有被正确构造");
} catch (IllegalArgumentException e) {
throw e;
//throw new TokenException("非法参数异常");
} catch (Exception e) {
throw e;
//throw new IllegalStateException("Invalid Token. "+e.getMessage());
}
return null;
}
return null;
}
}
JwtAccessDeniedHandler 无权限访问
import com.alibaba.fastjson.JSON;
import com.pinyi.security.security.enums.AuthorizationCodeEnum;
import com.pinyi.supply.common.http.HttpResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* @author :Shixing
* @date :Created in 2020/7/14 11:04
* @description:无权限访问拦截
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
System.out.println("无权限访问");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TEMPORARILY_WITHOUT_AUTHORITY)).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
JwtCustomLogoutHandler 自定义登出实现
import com.pinyi.supply.redis.enuns.RedisCacheEnum;
import com.pinyi.supply.redis.service.RedisService;
import com.pinyi.supply.system.dto.LoginDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class JwtCustomLogoutHandler implements LogoutHandler {
@Autowired
RedisService redisService;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
System.out.println("自定义登出方法拦截");
LoginDto loginDto = (LoginDto) authentication.getPrincipal();
//删除redis缓存
String[] keys = {
RedisCacheEnum.LOGIN_PATH.getPath() + loginDto.getSysUser().getLoginName() + RedisCacheEnum.USER_PATH.getPath(),
RedisCacheEnum.LOGIN_PATH.getPath() + loginDto.getSysUser().getLoginName() + RedisCacheEnum.PARAMETER_PATH.getPath(),
RedisCacheEnum.LOGIN_PATH.getPath() + loginDto.getSysUser().getLoginName() + RedisCacheEnum.TOKEN_PATH.getPath()
};
redisService.del(keys);
//TODO 记录登出日志
}
}
JWTLogoutSuccessHandler 登出成功拦截
@Component
public class JWTLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
JwtTokenUtils jwtUtils;
@Autowired
JwtConfig jwtConfig;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("登出方法拦截");
if (authentication != null) {
new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);
}
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
httpServletResponse.setHeader(jwtConfig.getHeader(), "");
outputStream.write(JSONUtil.toJsonStr(HttpResult.success("登出成功")).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
LoginFailureHandler 登录失败拦截
import cn.hutool.json.JSONUtil;
import com.pinyi.security.security.enums.AuthorizationCodeEnum;
import com.pinyi.security.security.exception.CaptchaException;
import com.pinyi.supply.common.http.HttpResult;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* 登录失败处理器
*
* @author shixing
* @create 2021-05-07-22:06
*/
@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");
String lang = request.getHeader("lang");
String msg = AuthorizationCodeEnum.UNKNOWN_ERROR.getMsg();
if (exception instanceof BadCredentialsException) {
msg = exception.getMessage();
} else if (exception instanceof CaptchaException) {
msg = exception.getMessage();
} else if (exception != null) {
msg = exception.getMessage();
// 可以添加更多的异常处理逻辑
}
//返回错误信息
try (ServletOutputStream outputStream = response.getOutputStream()) {
outputStream.write(JSONUtil.toJsonStr(HttpResult.error(msg)).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
// 抛出异常,终止后续过滤器链的执行
throw new ServletException("Authentication failed", exception);
}
}
LoginSuccessHandler 登录成功拦截
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.pinyi.security.security.config.JwtConfig;
import com.pinyi.security.security.entity.LoginUser;
import com.pinyi.security.security.utils.JwtTokenUtils;
import com.pinyi.supply.common.http.HttpResult;
import com.pinyi.supply.redis.enuns.RedisCacheEnum;
import com.pinyi.supply.redis.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
JwtTokenUtils jwtTokenUtils;
@Autowired
JwtConfig jwtConfig;
@Autowired
RedisService redisService;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功");
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
//通过了,生成jwt
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String token = jwtTokenUtils.generateToken(loginUser.getUsername());
//将用户信息存入redis
httpServletResponse.setHeader(jwtConfig.getHeader(), token);
redisService.set(RedisCacheEnum.LOGIN_PATH.getPath() + loginUser.getUsername() + RedisCacheEnum.USER_PATH.getPath(),
JSON.toJSONString(loginUser.getLoginDto()), jwtConfig.getExpired() + 600);
redisService.set(RedisCacheEnum.LOGIN_PATH.getPath() + loginUser.getUsername() + RedisCacheEnum.PARAMETER_PATH.getPath()
, JSON.toJSONString(loginUser.getLoginDto().getPermissions()), jwtConfig.getExpired() + 600);
redisService.set(RedisCacheEnum.LOGIN_PATH.getPath() + loginUser.getUsername() + RedisCacheEnum.TOKEN_PATH.getPath(),
token, jwtConfig.getExpired() + 600);
//删除验证码
redisService.del(RedisCacheEnum.CAPTCHA_PATH.getPath() + loginUser.getUsername());
outputStream.write(JSON.toJSONString(HttpResult.success("登录成功", token)).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
完结某些错误信息 自定义修改即可