JWT是在Web应用中安全传递信息的规范,从本质上来说是Token的演变
是一种生成加密用户身份信息的Token,特别适用于分布式单点登陆的场景
无需在服务端保存用户的认证信息,而是直接对Token进行校验获取用户信息
使单点登录更为简单灵活
三、身份验证架构
SecurityContextHolder
:Spring Security 存储份验证者详细信息的位置
SecurityContext
:从获取SecurityContextHolder并包含Authentication当前经过身份验证的用户的
Authentication
:可以是输入以AuthenticationManager提供用户提供的用于身份验证的凭据或来自SecurityContext
GrantedAuthority
:授予主体的权限Authentication(即角色、范围等)
AuthenticationManager
:定义 Spring Security 的过滤器如何执行身份验证的API
ProviderManager
:最常见的实现AuthenticationManager
AuthenticationProvider
:用于ProviderManager执行特定类型的身份验证
Request Credentials withAuthenticationEntryPoint
:用于从客户端请求凭据(即重定向到登录页面、发送WWW-Authenticate响应等)
AbstractAuthenticationProcessingFilter
:Filter用于身份验证的基础。这也很好地了解了身份验证的高级流程以及各个部分如何协同工作
四、前期准备
graph LR
用户登录认证 --> session或者token保存会话-->判断用户角色-->判断权限-->获取对应资源
🕰️ 最小数据模型
🕰️ 数据库脚本
🌳sys_user 🌳sys_role 🌳sys_menu 🌳sys_role_menu
👉 配置文件信息
user:
password:
maxRetryCount: 5
lockTime: 10
token:
header: Authorization
secret: K0TmVM#8O9u2end6V~QpYZ!!Xt
expireTime: 30
🕰️ 创建国际化文件
👉🏽 messages.properties**
#错误消息
not.null=* 必须填写
user.captcha.error=验证码错误
user.captcha.expire=验证码已失效
user.not.exists=用户不存在/密码错误
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
user.password.delete=对不起,您的账号已被删除
user.blocked=用户已封禁,请联系管理员
role.blocked=角色已封禁,请联系管理员
user.logout.success=退出成功
👉 spring工具类 方便在非spring管理环境中获取bean
👉 获取i18n资源文件
* Author: 花棉袄
* Date: 2022年08月20日
* Describe: 获取i18n资源文件
public class MessageUtils {
* 根据消息键和参数 获取消息 委托给spring messageSource
* @param code 消息键
* @param args 参数
* @return 获取国际化翻译值
public static String message(String code, Object... args) {
MessageSource messageSource = SpringUtils.getBean(MessageSource.class);
return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
🕰️ 登录用户身份权限
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class LoginUserDetail implements UserDetails {
private static final long serialVersionUID = 1L;
@ApiModelProperty("用户ID")
private Long userId;
@ApiModelProperty("部门ID")
private Long deptId;
@ApiModelProperty("用户唯一标识")
private String token;
@ApiModelProperty("登录时间")
private Date loginTime;
@ApiModelProperty("token的过期时间")
private Long expireTime;
@ApiModelProperty("登录IP地址")
private String ipAddress;
@ApiModelProperty("登录地点")
private String loginLocation;
@ApiModelProperty("浏览器类型")
private String browser;
@ApiModelProperty("操作系统")
private String os;
@ApiModelProperty("权限列表")
private Set<String> permissions;
@ApiModelProperty("用户信息")
private SysUser sysUser;
public LoginUserDetail(Long userId, Long deptId, SysUser sysUser, Set<String> permissions) {
this.userId = userId;
this.deptId = deptId;
this.permissions = permissions;
this.sysUser = sysUser;
public LoginUserDetail(Long userId, Set<String> permissions) {
this.userId = userId;
this.permissions = permissions;
* 授予用户权限
* @return 封装用户权限信息
@JSONField(serialize = false)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
@JSONField(serialize = false)
@Override
public String getPassword() {
return sysUser.getPassword();
@Override
public String getUsername() {
return sysUser.getUserName();
* 账户是否未过期,过期无法验证
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {
return true;
* 指定用户是否解锁,锁定的用户无法进行身份验证
* @return
@Override
public boolean isAccountNonLocked() {
return true;
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
* @return
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
* 是否可用 ,禁用的用户不能身份验证
* @return
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
👉🏽登录信息:LoginBody
* @Author Michale
* @CreateDate 2022/9/5
* @Describe 登录信息
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginBody implements Serializable {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("用户密码")
private String password;
@ApiModelProperty("验证码")
private String code;
@ApiModelProperty("唯一标识")
private String uuid;
👉 引入security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
🕰️ token工具服务类
public class TokenUtils {
* 从数据生成令牌
* @param claims 数据
* @return token
public String createToken(Map<String, Object> claims) {
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret())
.compact();
return token;
* 从令牌中获取数据
* @param token 令牌
* @return 数据声明
public Claims parseToken(String token) {
Claims body = Jwts.parser()
.setSigningKey(tokenProperties.getSecret())
.parseClaimsJws(token)
.getBody();
return body;
* 获取缓存的token键值
* @param uuid
* @return
public String getTokenKey(String uuid) {
return RedisCacheConstant.LOGIN_TOKEN_KEY + uuid;
五、认证处理方法
🧭 Jwt认证过滤器
首先用户请求进来直接拦截:从请求中获取token然后 ->从请求中获取用户身份信息
* @Author: 花棉袄
* @CreateDate: 2022年08月30日
* @Describe: token过滤器 验证token有效性
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Autowired
private TokenUtils tokenUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
LoginUserDetail loginUser = tokenUtils.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
tokenUtils.verifyToken(loginUser);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityUtils.setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request, response);
👉🏽TokenUtils工具类添加 刷新token有效期 的方法
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
* @param loginUserDetail
* @return 令牌
public void verifyToken(LoginUserDetail loginUserDetail) {
long expireTime = loginUserDetail.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(loginUserDetail);
* 刷新令牌有效期
* @param loginUserDetail
public void refreshToken(LoginUserDetail loginUserDetail) {
loginUserDetail.setLoginTime(System.currentTimeMillis());
loginUserDetail.setExpireTime(loginUserDetail.getLoginTime() + tokenProperties.getExpireTime() * MILLIS_MINUTE);
String userKey = getTokenKey(loginUserDetail.getToken());
redisUtils.setCacheObject(userKey, loginUserDetail, tokenProperties.getExpireTime(), TimeUnit.MINUTES);
👉🏽封装SecurityUtils工具类
我们首先创建一个空的SecurityContext. 重要的是创建一个新SecurityContext实例
SecurityContextHolder.getContext().setAuthentication(authentication)它来避免跨多个线程的竞争条件
* 获取Authentication
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
* 设置Authentication
* @return
public static void setAuthentication(UsernamePasswordAuthenticationToken authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
👉 TokenUtils添加相应的方法
* 从请求中获取用户身份信息
* @return 用户信息
public LoginUserDetail getLoginUser(HttpServletRequest request) {
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
try {
Claims claims = parseToken(token);
String uuid = (String) claims.get(CommonConstant.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUserDetail user = redisUtils.getCacheObject(userKey);
return user;
} catch (Exception e) {
return null;
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
* @param loginUserDetail
* @return 令牌
public void verifyToken(LoginUserDetail loginUserDetail) {
long expireTime = loginUserDetail.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(loginUserDetail);
* 刷新令牌有效期
* @param loginUserDetail
public void refreshToken(LoginUserDetail loginUserDetail) {
loginUserDetail.setLoginTime(System.currentTimeMillis());
loginUserDetail.setExpireTime(loginUserDetail.getLoginTime() + tokenProperties.getExpireTime() * MILLIS_MINUTE);
String userKey = getTokenKey(loginUserDetail.getToken());
redisUtils.setCacheObject(userKey, loginUserDetail, tokenProperties.getExpireTime(), TimeUnit.MINUTES);
🧭 认证失败处理类
* @Author: 花棉袄
* @CreateDate: 2022年08月30日
* @Describe: 认证失败处理类 返回未授权
@Component
public class AuthenticationError implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
int code = HttpStatus.UNAUTHORIZED.value();
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
👉🏽将失败信息渲染到客户端
* 将字符串渲染到客户端
* @param response 渲染对象
* @param string 待渲染的字符串
public static void renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
🧭 自定义退出处理类
* @Author: 花棉袄
* @CreateDate: 2022年08月30日
* @Describe: 自定义退出处理类 返回成功
@Configuration
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {
@Autowired
private TokenUtils tokenUtils;
* 退出处理
* @return
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
LoginUserDetail loginUserDetail = tokenUtils.getLoginUser(request);
if (StringUtils.isNotNull(loginUserDetail)) {
String userName = loginUserDetail.getUsername();
tokenUtils.delLoginUser(loginUserDetail.getToken());
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HTTPCodeMessage.SUCCESS.getCode(), "退出成功")));
👉 TokenUtils添加相应的方法
* 删除用户身份信息
public void delLoginUser(String token) {
if (StringUtils.isNotEmpty(token)) {
String userKey = getTokenKey(token);
redisUtils.deleteObject(userKey);
六、授权处理
🍑 登录用户身份信息
@Slf4j
@Service("userDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
SysUser user = sysUserService.selectUserByUserName(username);
if (StringUtils.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("用户验证处理", "登录用户:" + username + " 不存在");
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException("用户验证处理", "对不起,您的账号:" + username + " 已停用");
passwordService.validate(user);
return createLoginUser(user);
public UserDetails createLoginUser(SysUser user) {
return new LoginUserDetail(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
permissionService.getMenuPermission(user):从数据库中查询权限标识
🍑 封装用户权限信息
* @author 沉默的羔羊
* 2022年9月03日
* @apiNote 授权信息封装
@Service("permissionService")
public class PermissionServiceImpl implements PermissionService {
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
* 获取角色数据权限
* @param sysUser 用户信息
* @return 角色数据权限 ("admin 或者 common ...")
@Override
public Set<String> getRolePermission(SysUser sysUser) {
Set<String> roles = new HashSet<String>();
if (SecurityUtils.isAdmin(sysUser.getIdentity())) {
roles.add("admin");
} else {
Set<String> permission = sysRoleService.selectPermissionByUserId(sysUser.getUserId());
roles.addAll(permission);
return roles;
* 获取菜单数据权限
* @param sysUser 用户信息
* @return 菜单数据权限 ("*.*.* 或者 system:user:list ...")
public Set<String> getMenuPermission(SysUser sysUser) {
Set<String> menuPermission = new HashSet<String>();
if (SecurityUtils.isAdmin(sysUser.getIdentity())) {
menuPermission.add("*:*:*");
} else {
List<SysRole> roles = sysUser.getSysRoleList();
if (!roles.isEmpty() &&roles.size() > 1) {
for (SysRole sysRole : roles) {
Set<String> roleMenuPerms = sysMenuService.selectMenuPermsByRoleId(sysRole.getRoleId());
sysRole.setPermissions(roleMenuPerms);
menuPermission.addAll(roleMenuPerms);
} else {
Set<String> roleMenuPerms = sysMenuService.selectMenuPermsByUserId(sysUser.getUserId());
menuPermission.addAll(roleMenuPerms);
return menuPermission;
🍑 密码校验实现类
👉🏽首先创建一个身份验证信息线程
- SecurityContextHolder使用 ThreadLocal来存储这些详细信息
- 这意味着SecurityContext始终可用于同一线程中的方法
- 即使SecurityContext未明确将其作为参数传递给这些方法
- ThreadLocal如果在处理当前主体的请求后注意清除线程
- 那么以这种方式使用是非常安全的
👉Authentication包含
principal:识别用户。当使用用户名/密码进行身份验证时,这通常是UserDetails
credentials:通常是密码。在许多情况下,这将在用户通过身份验证后被清除,以确保它不被泄露
authorities:GrantedAuthoritys是授予用户的高级权限,一些示例是角色或范围
* @Author: 花棉袄
* @CreateDate: 2022年08月30日
* @Describe: 身份验证信息线程
public class AuthenticationContext {
private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();
public static Authentication getContext() {
return contextHolder.get();
public static void setContext(Authentication context) {
contextHolder.set(context);
public static void clearContext() {
contextHolder.remove();
👉🏽校验密码时:使用线程获取Authentication
Authentication usernamePasswordAuthenticationToken = AuthenticationContext.getContext();
String inUserName = usernamePasswordAuthenticationToken.getName();
String inPassWord = usernamePasswordAuthenticationToken.getCredentials().toString();
👉密码校验实现类
@Slf4j
@Service("permissionService")
public class PasswordServiceImpl implements PasswordService {
@Autowired
private RedisUtils redisUtils;
@Autowired
private PassWordProperties passWordProperties;
* 登录密码方法
* @param user 通过用户名查询到的用户信息
@Override
public void validate(SysUser user) {
Authentication usernamePasswordAuthenticationToken = AuthenticationContext.getContext();
String inUserName = usernamePasswordAuthenticationToken.getName();
String inPassWord = usernamePasswordAuthenticationToken.getCredentials().toString();
Integer retryCount = getRetryCount(inUserName);
if (retryCount == null) {
retryCount = 0;
if (retryCount >= passWordProperties.getMaxRetryCount()) {
String message = MessageUtils.message("user.password.retry.limit.exceed");
log.info(message);
throw new UserPasswordMaxNumberExceptions(passWordProperties.getMaxRetryCount(), passWordProperties.getLockTime());
if (!isCheckPassword(user, inPassWord)) {
String message = MessageUtils.message("user.password.retry.limit.count", retryCount);
cacheToRedis(inUserName, retryCount);
throw new UserPasswordNotMatchException();
} else {
clearLoginRecordCache(inUserName);
* 登录账户密码错误次数缓存键名
* @param userName 用户名
* @return 缓存键key
private String getCacheKey(String userName) {
return RedisCacheConstant.PWD_ERR_CNT_KEY + userName;
* 查询密码错误输入次数
* @param inUserName 登录时输入的账户名
* @return
private Integer getRetryCount(String inUserName) {
return redisUtils.getCacheObject(getCacheKey(inUserName));
* 将输入次数缓存到redis中
* @param userName 登录时输入的账户名
* @param retryCount 记录错误次数
private void cacheToRedis(String userName, Integer retryCount) {
redisUtils.setCacheObject(getCacheKey(userName), retryCount, passWordProperties.getLockTime(), TimeUnit.MINUTES);
* 进行密码比对
* @param user 通过用户名查询到的账户信息
* @param inPassWord 用户登录时输入的信息
* @return
private boolean isCheckPassword(SysUser user, String inPassWord) {
boolean isCheckPassword = SecurityUtils.matchesPassword(inPassWord, user.getPassword());
return isCheckPassword;
* 清空记录
* @param userName 登录时输入的账户名
private void clearLoginRecordCache(String userName) {
if (redisUtils.hasKey(getCacheKey(userName))) {
redisUtils.deleteObject(getCacheKey(userName));
👉🏽封装SecurityUtils工具类
* 生成BCryptPasswordEncoder密码
* @param password 密码
* @return 加密字符串
public static String encryptPassword(String password) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.encode(password);
* 判断密码是否相同
* @param rawPassword 真实密码
* @param encodedPassword 加密后字符
* @return 结果
public static boolean matchesPassword(String rawPassword, String encodedPassword) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.matches(rawPassword, encodedPassword);
七、核心配置类
🍎 匿名访问不鉴权注解
* @Author: 花棉袄
* @CreateDate: 2022年08月30日
* @Describe: 匿名访问不鉴权注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Anonymous {
👉🏽 获取Anonymous注解的路径
* @Author: 花棉袄
* @CreateDate: 2022年08月30日
* @Describe: 获取Anonymous注解的路径
@Component
public class Anonymous {
* 获取标有注解 AnonymousAccess 的访问路径
public static String[] getAnonymousUrls() {
Map<RequestMappingInfo, HandlerMethod> handlerMethods = SpringUtils.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
Set<String> allAnonymousAccess = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethods.entrySet()) {
HandlerMethod value = infoEntry.getValue();
com.michale.framework.security.annotation.Anonymous methodAnnotation = value.getMethodAnnotation(com.michale.framework.security.annotation.Anonymous.class);
if (methodAnnotation != null) {
allAnonymousAccess.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
return allAnonymousAccess.toArray(new String[0]);
🍎 配置类 SecurityConfig
👉🏽SecurityConfig:配置类
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
👉🏽解决 无法直接注入 AuthenticationManager
* 解决 无法直接注入 AuthenticationManager
* @return
* @throws Exception
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
👉🏽 强散列哈希加密实现
* 强散列哈希加密实现
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
* 身份认证接口
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
👉🏽实现:configure
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/login", "/register", "/captchaImage").anonymous()
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
.antMatchers(Anonymous.getAnonymousUrls()).anonymous()
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
httpSecurity.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity.addFilterBefore(corsFilter, JwtAuthorizationFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
👉🏽配置加密规则
* 强散列哈希加密实现
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
* 身份认证接口
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
八、登录接口
🍏 用户登录实现
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContext.setContext(authenticationToken);
authentication = authenticationManager.authenticate(authenticationToken);
👉🏽 校验验证码的方法
* 校验验证码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
public void validateCaptcha(String code, String uuid) {
String verifyKey = RedisCacheConstant.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
String captcha = redisUtils.getCacheObject(verifyKey);
redisUtils.deleteObject(verifyKey);
if (captcha == null) {
throw new CaptchaExpireException();
if (!code.equalsIgnoreCase(captcha)) {
throw new CaptchaException();
👉🏽 鉴权成功之后就可以生成token
// 生成token
LoginUserDetail loginUserDetail = (LoginUserDetail) authentication.getPrincipal()
return tokenUtils.createToken(loginUserDetail)
* 创建令牌
* @param loginUserDetail 用户信息
* @return 令牌
public String createToken(LoginUserDetail loginUserDetail) {
String token = IdUtils.fastUUID();
loginUserDetail.setToken(token);
setUserAgent(loginUserDetail);
refreshToken(loginUserDetail);
Map<String, Object> claims = new HashMap<>();
claims.put(CommonConstant.LOGIN_USER_KEY, token);
return createToken(claims);
* 设置用户代理信息
* @param loginUserDetail 登录信息
public void setUserAgent(LoginUserDetail loginUserDetail) {
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = IpUtils.getIpAddress(ServletUtils.getRequest());
loginUserDetail.setIpaddr(ip);
loginUserDetail.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUserDetail.setBrowser(userAgent.getBrowser().getName());
loginUserDetail.setOs(userAgent.getOperatingSystem().getName());
🍏 登陆成功之后
登陆成功之后可以从:Authentication获取用户信息
@GetMapping("/getInfo")
public AjaxResult getInfo() {
SysUser user = SecurityUtils.getLoginUser().getUser()
// 角色集合
Set<String> roles = permissionService.getRolePermission(user)
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user)
AjaxResult ajax = AjaxResult.success()
ajax.put("user", user)
ajax.put("roles", roles)
ajax.put("permissions", permissions)
return ajax
👉🏽 SecurityUtils添加获取用户信息的方法
* 获取用户
public static LoginUserDetail getLoginUser() {
try {
return (LoginUserDetail) getAuthentication().getPrincipal();
} catch (Exception e) {
throw new ServiceException("SecurityUtils_GetUserDetail", "获取用户信息异常", HTTPCodeMessage.UNAUTHORIZED.getCode());
九、自定义权限注解
👉🏽PermissionContextHolder权限信息
* @Author Michale
* @CreateDate 2022/9/14
* @Describe 权限信息
public class PermissionContextHolder {
private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT";
public static void setContext(String permission) {
RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission,
RequestAttributes.SCOPE_REQUEST);
public static String getContext() {
return ClassConvert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES,
RequestAttributes.SCOPE_REQUEST));
🍓 自定义权限校验
@Service("security")
public class MyPermissionServiceImpl implements MyPermissionService {
* 所有权限标识
private static final String ALL_PERMISSION = "*:*:*";
* 管理员角色权限标识
private static final String SUPER_ADMIN = "admin";
private static final String ROLE_DELIMITER = ",";
private static final String PERMISSION_DELIMITER = ",";
* 判断是否包含权限
* @param permissions 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
public boolean hasPermissions(Set<String> permissions, String permission) {
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
👉🏽验证用户是否具备某权限
* 验证用户是否具备某权限
* @param permission 权限字符串
* @return 用户是否具备某权限
@Override
public boolean isPermission(String permission) {
if (StringUtils.isEmpty(permission)) {
return false;
LoginUserDetail loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNull(loginUser) || StringUtils.isEmpty(loginUser.getPermissions())) {
return false;
PermissionContextHolder.setContext(permission);
boolean isPermission = hasPermissions(loginUser.getPermissions(), permission);
return isPermission;
👉🏽验证用户是否不具备某权限
* 验证用户是否不具备某权限
* @param permission 权限字符串
* @return 用户是否不具备某权限
@Override
public boolean notPermission(String permission) {
return isPermission(permission);
👉🏽判断用户是否拥有某个角色
* 判断用户是否拥有某个角色
* @param permission 角色字符串
* @return 用户是否拥有某个角色
@Override
public boolean hasRole(String permission) {
if (StringUtils.isEmpty(permission)) {
return false;
LoginUserDetail loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNull(loginUser) || StringUtils.isEmpty(loginUser.getUserVo().getRoles())) {
return false;
for (RoleVo role : loginUser.getUserVo().getRoles()) {
String roleKey = role.getRoleKey();
if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(permission))) {
return true;
return false;
👉🏽判断用户是否不拥有某个角色
* 判断用户是否不拥有某个角色
* @param permission 角色字符串
* @return boolean
@Override
public boolean notHasRole(String permission) {
return hasRole(permission);
🍓 使用自定义的权限校验
@GetMapping("/hello")
@PreAuthorize("@security.hasRole('admin')")
@ApiOperation(value = "角色信息表查看详情", notes = "SysRole")
public AjaxResult getSysRoleById() {
return AjaxResult.success().put("hello", "hello");
@GetMapping("/hello")
@PreAuthorize("@security.isPermission('system:user:query')")
@ApiOperation(value = "角色信息表查看详情", notes = "SysRole")
public AjaxResult getSysRoleById() {
return AjaxResult.success().put("hello", "hello");
"msg": "没有权限,要求用户的身份认证",
"code": 401,
"/system/hello": "不允许访问"
十、权限校验拦截器
@ApiModelProperty("权限校验异常")
@ExceptionHandler(AccessDeniedException.class)
public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址:'{}',权限校验失败:'{}'", requestURI, e.getMessage());
return AjaxResult.error(UNAUTHORIZED.getCode(), UNAUTHORIZED.getMessage())
.put(requestURI, e.getMessage());
🚦 报错问题解决
Error creating bean with name 'springSecurityFilterChain' ........
🍒添加以下配置信息
spring:
pathmatch:
matching-strategy: ant-path-matcher
复制代码