当前位置: 首页 > news >正文

若依解析(一)登录认证流程

JWT+SpringSecurity 6.X 实现登录

JWT token只包含uuid ,token 解析uuid,然后某个常量加UUID 从Redis缓存查询用户信息
流程图如下

登录流程简画

感谢若依,感谢开源,能有这么好系统供我学习。

设计数据库,部门表,用户表,角色表,权限菜单表,用户角色表中间表,部门角色中间表,角色权限菜单表中间表

登录成功返回前端

{"msg": "操作成功","code": "200",//用户tokentoken:"XXXXX",//菜单权限"permissions": ["system:user:resetPwd","system:post:list","monitor:operlog:export","monitor:druid:list","system:menu:query","system:dept:remove","system:menu:list",..."tool:gen:edit"],//角色权限"roles": ["test2","test1"],//用户信息"user": {"createBy": "admin","createTime": "2024-11-28 10:06:12",}//路由信息[{"name": "System","path": "/system","hidden": false,"redirect": "noRedirect","component": "Layout","alwaysShow": true,"meta": {"title": "系统管理","icon": "system","noCache": false,"link": null},"children": [{"name": "User","path": "user","hidden": false,"component": "system/user/index","meta": {"title": "用户管理","icon": "user","noCache": false,"link": null}},{"name": "Log","path": "log","hidden": false,"redirect": "noRedirect","component": "ParentView","alwaysShow": true,"meta": {"title": "日志管理","icon": "log","noCache": false,"link": null},"children": [{"name": "Operlog","path": "operlog","hidden": false,"component": "monitor/operlog/index","meta": {"title": "操作日志","icon": "form","noCache": false,"link": null}},{"name": "Logininfor","path": "logininfor","hidden": false,"component": "monitor/logininfor/index","meta": {"title": "登录日志","icon": "logininfor","noCache": false,"link": null}}]}]},{"name": "Tool","path": "/tool","hidden": false,"redirect": "noRedirect","component": "Layout","alwaysShow": true,"meta": {"title": "系统工具","icon": "tool","noCache": false,"link": null},"children": [{"name": "Build","path": "build","hidden": false,"component": "tool/build/index","meta": {"title": "表单构建","icon": "build","noCache": false,"link": null}},{"name": "Gen","path": "gen","hidden": false,"component": "tool/gen/index","meta": {"title": "代码生成","icon": "code","noCache": false,"link": null}},{"name": "Swagger","path": "swagger","hidden": false,"component": "tool/swagger/index","meta": {"title": "系统接口","icon": "swagger","noCache": false,"link": null}}]},{"name": "Http://ruoyi.vip","path": "http://ruoyi.vip","hidden": false,"component": "Layout","meta": {"title": "若依官网","icon": "guide","noCache": false,"link": "http://ruoyi.vip"}}]
}

所有实体的基类

/*** 实体基类**/
@Setter
@Getter
@ToString
public class BaseEntity 
{private static final long serialVersionUID = 1L;/** 搜索值 */private String searchValue;/** 创建者 */private String createBy;/** 创建时间 */@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date createTime;/** 更新者 */private String updateBy;/** 更新时间 */@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date updateTime;/** 备注 */@ApiModelProperty(value = "员工姓名",hidden = true)private String remark;/*** 更新数据**/@ApiModelProperty(value = "员工姓名",hidden = true)private boolean update;/*** 新增数据*/@ApiModelProperty(value = "员工姓名",hidden = true)private boolean add;/** 请求参数 */@JsonInclude(JsonInclude.Include.NON_EMPTY)@ApiModelProperty(value = "请求参数",hidden = true)private Map<String, Object> params;}

部门实体

/*** 部门表 sys_dept**/
@Setter
@Getter
@ToString
public class SysDept extends BaseEntity
{private static final long serialVersionUID = 1L;/** 部门ID */private Long deptId;/** 父部门ID */private Long parentId;/** 祖级列表 */private String ancestors;/** 部门名称 */private String deptName;/** 显示顺序 */private Integer orderNum;/** 负责人 */private String leader;/** 联系电话 */private String phone;/** 邮箱 */private String email;/** 部门状态:0正常,1停用 */private String status;/** 删除标志(0代表存在 2代表删除) */private String delFlag;/** 父部门名称 */private String parentName;/** 子部门 */private List<SysDept> children = new ArrayList<SysDept>();
}

用户实体包含角色集合,以及所属部门信息

/*** 用户对象 sys_user**/
@Setter
@Getter
@ToString
public class SysUser extends BaseEntity
{private static final long serialVersionUID = 1L;/** 用户ID */private Long userId;/** 部门ID */private Long deptId;/** 用户账号 */private String userName;/** 用户昵称 */private String nickName;/** 用户邮箱 */private String email;/** 手机号码 */private String phonenumber;/** 用户性别 */private String sex;/** 用户头像 */private String avatar;/** 密码 */private String password;/** 帐号状态(0正常 1停用) */private String status;/** 删除标志(0代表存在 2代表删除) */private String delFlag;/** 最后登录IP */private String loginIp;/** 最后登录时间 */private Date loginDate;/** 部门对象 */private SysDept dept;/** 角色对象 */private List<SysRole> roles;/** 角色组 */private Long[] roleIds;/** 岗位组 */private Long[] postIds;/** 角色ID */private Long roleId;}

角色实体包含权限按钮集合,每个按钮对应一个权限(system:user:add),所以角色还包含权限集合

/*** 角色表 sys_role**/
@Setter
@Getter
@ToString
public class SysRole extends BaseEntity
{private static final long serialVersionUID = 1L;/** 角色ID */private Long roleId;/** 角色名称 */private String roleName;/** 角色权限 */private String roleKey;/** 角色排序 */private Integer roleSort;/** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */private String dataScope;/** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */private boolean menuCheckStrictly;/** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */private boolean deptCheckStrictly;/** 角色状态(0正常 1停用) */private String status;/** 删除标志(0代表存在 2代表删除) */private String delFlag;/** 用户是否存在此角色标识 默认不存在 */private boolean flag = false;/** 菜单组 */private Long[] menuIds;/** 部门组(数据权限) */private Long[] deptIds;/** 角色菜单权限 */private Set<String> permissions;}

按钮权限实体,一个按钮下面还有子菜单,所以还包含一个菜单集合

/*** 菜单权限表 sys_menu**/
@Setter
@Getter
@ToString
public class SysMenu extends BaseEntity
{private static final long serialVersionUID = 1L;/** 菜单ID */private Long menuId;/** 菜单名称 */private String menuName;/** 父菜单名称 */private String parentName;/** 父菜单ID */private Long parentId;/** 显示顺序 */private Integer orderNum;/** 路由地址 */private String path;/** 组件路径 */private String component;/** 路由参数 */private String query;/** 路由名称,默认和路由地址相同的驼峰格式(注意:因为vue3版本的router会删除名称相同路由,为避免名字的冲突,特殊情况可以自定义) */private String routeName;/** 是否为外链(0是 1否) */private String isFrame;/** 是否缓存(0缓存 1不缓存) */private String isCache;/** 类型(M目录 C菜单 F按钮) */private String menuType;/** 显示状态(0显示 1隐藏) */private String visible;/** 菜单状态(0正常 1停用) */private String status;/** 权限字符串 */private String perms;/** 菜单图标 */private String icon;/** 子菜单 */private List<SysMenu> children = new ArrayList<SysMenu>();}

登录用户省份鉴权实体,继承UserDetails来源于 spring security

这个用户包含数据库中用户信息,而用户信息又包含角色,而角色又包含权限菜单,所以这个自定义UserDetails实现类包含用户的所有信息

/*** 登录用户身份权限* */
@Setter
@Getter
@ToString
public class LoginUser implements UserDetails
{private static final long serialVersionUID = 1L;/*** 用户ID*/private Long userId;private String userName;/*** 部门ID*/private Long deptId;/*** 用户唯一标识*/private String token;/*** 登录时间*/private Long loginTime;/*** 过期时间*/private Long expireTime;/*** 登录IP地址*/private String ip;/*** 登录地点*/private String loginLocation;/*** 浏览器类型*/private String browser;/*** 操作系统*/private String os;/*** 权限列表*/private Set<String> permissions;/*** 用户信息*/private SysUser user;@JSONField(serialize = false)@Overridepublic String getPassword(){return user.getPassword();}@Overridepublic String getUsername(){return user.getUserName();}/*** 账户是否未过期,过期无法验证*/@JSONField(serialize = false)@Overridepublic boolean isAccountNonExpired(){return true;}/*** 指定用户是否解锁,锁定的用户无法进行身份验证* * @return*/@JSONField(serialize = false)@Overridepublic boolean isAccountNonLocked(){return true;}/*** 指示是否已过期的用户的凭据(密码),过期的凭据防止认证* * @return*/@JSONField(serialize = false)@Overridepublic boolean isCredentialsNonExpired(){return true;}/*** 是否可用 ,禁用的用户不能身份验证* * @return*/@JSONField(serialize = false)@Overridepublic boolean isEnabled(){return true;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities(){return null;}
}

导入spring-security 6.x 依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>

spring-security 上下文,上下文管理对象,用来在程序任何地方获取Authentication

/***  自定义 spring security 上下文管理类,管理Authentication对象**/
public class AuthenticationContextHolder
{//线程池保证线程传递同一个Authentication对象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();}
}

登录验证

/*** 登录验证** @param username 用户名* @param password 密码* @param code 验证码* @param uuid 唯一标识* @return 结果*/public String login(String username, String password, String code, String uuid){// 验证码校验validateCaptcha(username, code, uuid);// 登录前置校验loginPreCheck(username, password);// 用户验证Authentication authentication = null;try{//创建未认证令牌UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);AuthenticationContextHolder.setContext(authenticationToken);// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername,真正通过用户名查询数据库,然后在比较密码//密码一致,将UserDetail 对象转换为Authentication 对象authentication = authenticationManager.authenticate(authenticationToken);}catch (Exception e){if (e instanceof BadCredentialsException){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}else{AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}}finally{AuthenticationContextHolder.clearContext();}AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));LoginUser loginUser = (LoginUser) authentication.getPrincipal();recordLoginInfo(loginUser.getUserId());// 验证成功生成tokenreturn tokenService.createToken(loginUser);}

自定义UserDetailsServiceImpl,就是验证登录密码与从数据库中通过用户名查询的登录密码是否一致,一致的话再查询用户所有权限,

在把用户信息封装成UserDetail 返回

/*** 用户验证处理**/
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);@Autowiredprivate ISysUserService userService;@Autowiredprivate SysPasswordService passwordService;@Autowiredprivate SysPermissionService permissionService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{SysUser user = userService.selectUserByUserName(username);if (StringUtils.isNull(user)){log.info("登录用户:{} 不存在.", username);throw new ServiceException(MessageUtils.message("user.not.exists"));}else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())){log.info("登录用户:{} 已被删除.", username);throw new ServiceException(MessageUtils.message("user.password.delete"));}else if (UserStatus.DISABLE.getCode().equals(user.getStatus())){log.info("登录用户:{} 已被停用.", username);throw new ServiceException(MessageUtils.message("user.blocked"));}//验证登录密码与通过登录用户查询到的密码是否一致passwordService.validate(user);//密码验证成功,查询用户权限 ,将用户的所有信息封装成UserDetails 对象在返回return createLoginUser(user);}public UserDetails createLoginUser(SysUser user){//查询用户权限,返回 LoginUserreturn new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));}
}

查询用户权限

/*** 获得用户权限**/
@Component
public class SysPermissionService
{@Autowiredprivate ISysRoleService roleService;@Autowiredprivate ISysMenuService menuService;/*** 获取角色数据权限** @param user 用户信息* @return 角色权限信息*/public Set<String> getRolePermission(SysUser user){Set<String> roles = new HashSet<String>();// 管理员拥有所有权限if (user.isAdmin()){roles.add("admin");}else{roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));}return roles;}/*** 获取菜单数据权限** @param user 用户信息* @return 菜单权限信息*/public Set<String> getMenuPermission(SysUser user){Set<String> perms = new HashSet<String>();// 管理员拥有所有权限if (user.isAdmin()){perms.add("*:*:*");}else{List<SysRole> roles = user.getRoles();if (!CollectionUtils.isEmpty(roles)){// 多角色设置permissions属性,以便数据权限匹配权限for (SysRole role : roles){if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL)){//通过角色获取角色下的菜单权限Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());role.setPermissions(rolePerms);perms.addAll(rolePerms);}}}else{perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));}}return perms;}
}

登录密码验证

/*** 验证登录密码**/
@Component
public class SysPasswordService
{@Autowiredprivate RedisCache redisCache;//最大错误登录次数@Value(value = "${user.password.maxRetryCount}")private int maxRetryCount;//最大错误登录次数已满,固定时间才能重新登录@Value(value = "${user.password.lockTime}")private int lockTime;/*** 登录账户密码错误次数缓存键名** @param username 用户名* @return 缓存键key*/private String getCacheKey(String username){return CacheConstants.PWD_ERR_CNT_KEY + username;}public void validate(SysUser user){Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();String username = usernamePasswordAuthenticationToken.getName();String password = usernamePasswordAuthenticationToken.getCredentials().toString();//获取用户名密码错误登录次数Integer retryCount = redisCache.getCacheObject(getCacheKey(username));if (retryCount == null){retryCount = 0;}//用户名密码错误登录次数 大于最大登录次数返回提示10分钟后登录if (retryCount >= Integer.valueOf(maxRetryCount).intValue()){throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);}if (!matches(user, password)){//错误登录失败次数+1,返回登录失败retryCount = retryCount + 1;redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);throw new UserPasswordNotMatchException();}else{//登录成功,删除错误登录次数clearLoginRecordCache(username);}}public boolean matches(SysUser user, String rawPassword){//上下文中密码,与数据库中密码做比较return SecurityUtils.matchesPassword(rawPassword, user.getPassword());}//登录成功删除之前错误登录次数public void clearLoginRecordCache(String loginName){if (redisCache.hasKey(getCacheKey(loginName))){redisCache.deleteObject(getCacheKey(loginName));}}
}

登录验证完成

创建随机uuid,赋值loginUser.token

常量+uuid作为key,将用户信息保存redis

常量+uuid 加入JWTclaims, 创建加密后的JWT token

TokenService/*** 创建令牌** @param loginUser 用户信息* @return 令牌*/public String createToken(LoginUser loginUser){//生成随机UUIDString token = IdUtils.fastUUID();loginUser.setToken(token);setUserAgent(loginUser);//常量+uuid作为key,将用户信息保存redisrefreshToken(loginUser);Map<String, Object> claims = new HashMap<>();claims.put(Constants.LOGIN_USER_KEY, token);//JWT,常量+UUID加密创建tokenreturn createToken(claims);}/*** 刷新令牌有效期,第一次就是保存** @param loginUser 登录信息*/public void refreshToken(LoginUser loginUser){loginUser.setLoginTime(System.currentTimeMillis());loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);// 根据uuid将loginUser缓存String userKey = getTokenKey(loginUser.getToken());//某个常量加uuid 作为key,将用户信息保存redisredisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);}/*** JWT从数据声明生成令牌** @param claims 数据声明* @return 令牌*/private String createToken(Map<String, Object> claims){String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();return token;}

用户请求,被JwtAuthenticationTokenFilter过滤

/*** token过滤器 验证token有效性**/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{@Autowiredprivate TokenService tokenService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException{//从请求头中获得token,解析token,获得uuid,通过uuid 获得redis缓存中loginUser对象LoginUser loginUser = tokenService.getLoginUser(request);if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())){//验证token 失效时间,不足20分钟自动刷新tokenService.verifyToken(loginUser);//创建一个新的已认证令牌UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());//设置已认证令牌的DetailsauthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));//将已认证令牌设置到安全上下文SecurityContextHolder.getContext().setAuthentication(authenticationToken);}//请求转发给过滤器链下一个filter , 如果没有filter那就是请求的资源chain.doFilter(request, response);}
}

获得请求头里面token,TokenService类

	 /*** 获取用户身份信息** @return 用户信息*/public LoginUser getLoginUser(HttpServletRequest request){// 获取请求头中tokenString token = getToken(request);if (StringUtils.isNotEmpty(token)){try{Claims claims = parseToken(token);// 获得token中的uuidString uuid = (String) claims.get(Constants.LOGIN_USER_KEY);String userKey = getTokenKey(uuid);// 从缓存中获得LoginUser对象LoginUser user = redisCache.getCacheObject(userKey);return user;}catch (Exception e){log.error("获取用户信息异常'{}'", e.getMessage());}}return null;}/*** 获取请求token** @param request* @return token*/private String getToken(HttpServletRequest request){//从请求头中获取Authorization对应的值String token = request.getHeader(header);//判断请求头中是否有Authorization键值对,并且值以Bearer开头if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)){//将Bearer替换为空串,获得真正的token值token = token.replace(Constants.TOKEN_PREFIX, "");}return token;}

退出操作,直接实现LogoutSuccessHandler ,然后加入到SecurityConfig 过滤链中

/*** 自定义退出处理类 返回成功* */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{@Autowiredprivate TokenService tokenService;/*** 退出处理* * @return*/@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException, ServletException{LoginUser loginUser = tokenService.getLoginUser(request);if (StringUtils.isNotNull(loginUser)){String userName = loginUser.getUsername();// 删除用户缓存记录tokenService.delLoginUser(loginUser.getToken());// 记录用户退出日志AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));}ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success"))));}
}

定义权限上下文持有者,将参数放入RequestContextHolder,或者从RequestContextHolder取出,简单来说就是

属性与当前HTTP请求相关联的方法

/*** 权限信息* */
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 Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES,RequestAttributes.SCOPE_REQUEST));}
}

自定义权限注解,就是判断注解上权限值,与用户权限是否一致

/**自定义权限注解* * */
@Service("ss")
public class PermissionService
{/*** 验证用户是否具备某权限** @param permission 权限字符串* @return 用户是否具备某权限*/public boolean hasPermi(String permission){if (StringUtils.isEmpty(permission)){return false;}//从上下文文中获取登录对象LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}PermissionContextHolder.setContext(permission);//注解权限与用户权限做比较return hasPermissions(loginUser.getPermissions(), permission);}/*** 验证用户是否不具备某权限,与 hasPermi逻辑相反** @param permission 权限字符串* @return 用户是否不具备某权限*/public boolean lacksPermi(String permission){return hasPermi(permission) != true;}/*** 验证用户是否具有以下任意一个权限** @param permissions 以 PERMISSION_DELIMETER 为分隔符的权限列表* @return 用户是否具有以下任意一个权限*/public boolean hasAnyPermi(String permissions){if (StringUtils.isEmpty(permissions)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}PermissionContextHolder.setContext(permissions);Set<String> authorities = loginUser.getPermissions();for (String permission : permissions.split(Constants.PERMISSION_DELIMETER)){if (permission != null && hasPermissions(authorities, permission)){return true;}}return false;}/*** 判断用户是否拥有某个角色** @param role 角色字符串* @return 用户是否具备某角色*/public boolean hasRole(String role){if (StringUtils.isEmpty(role)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}for (SysRole sysRole : loginUser.getUser().getRoles()){String roleKey = sysRole.getRoleKey();if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))){return true;}}return false;}/*** 验证用户是否不具备某角色,与 isRole逻辑相反。** @param role 角色名称* @return 用户是否不具备某角色*/public boolean lacksRole(String role){return hasRole(role) != true;}/*** 验证用户是否具有以下任意一个角色** @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表* @return 用户是否具有以下任意一个角色*/public boolean hasAnyRoles(String roles){if (StringUtils.isEmpty(roles)){return false;}//从上下文中获取LoginUserLoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}for (String role : roles.split(Constants.ROLE_DELIMETER)){if (hasRole(role)){return true;}}return false;}/*** 判断是否包含权限** @param permissions 权限列表* @param permission 权限字符串* @return 用户是否具备某权限*/private boolean hasPermissions(Set<String> permissions, String permission){return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));}
}

自定义注解实现匿名访问

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Anonymous
{
}

注解匿名访问配置类,获得注解方法上的url

@Configuration
public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware
{private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");private ApplicationContext applicationContext;private List<String> urls = new ArrayList<>();public String ASTERISK = "*";@Overridepublic void afterPropertiesSet(){RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();map.keySet().forEach(info -> {HandlerMethod handlerMethod = map.get(info);// 获取方法上边的注解 替代path variable 为 *Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns()).forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));// 获取类上边的注解, 替代path variable 为 *Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns()).forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));});}@Overridepublic void setApplicationContext(ApplicationContext context) throws BeansException{this.applicationContext = context;}public List<String> getUrls(){return urls;}public void setUrls(List<String> urls){this.urls = urls;}
}

使用注解

@PostMapping("/url")
@Anonymous()
public AjaxResult anonymouTest(){return AjaxResult.success("操作成功");
}

SpringSecurity 配置文件

//prePostEnabled = true 时,@PreAuthorize 用于在方法执行前进行权限检查,@PostAuthorize 用于在方法执行后进行权限检查。
//securedEnabled = true 时,@Secured 用于允许具有特定角色的用户访问。
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration
public class SecurityConfig
{/*** 自定义用户认证逻辑*/@Autowiredprivate UserDetailsService userDetailsService;/*** 认证失败处理类*/@Autowiredprivate AuthenticationEntryPointImpl unauthorizedHandler;/*** 退出处理类*/@Autowiredprivate LogoutSuccessHandlerImpl logoutSuccessHandler;/*** token认证过滤器*/@Autowiredprivate JwtAuthenticationTokenFilter authenticationTokenFilter;/*** 跨域过滤器*/@Autowiredprivate CorsFilter corsFilter;/*** 允许匿名访问的地址*/@Autowiredprivate PermitAllUrlProperties permitAllUrl;/*** 身份验证实现*/@Beanpublic AuthenticationManager authenticationManager(){DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setUserDetailsService(userDetailsService);daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());return new ProviderManager(daoAuthenticationProvider);}/*** anyRequest          |   匹配所有请求路径* access              |   SpringEl表达式结果为true时可以访问* anonymous           |   匿名可以访问* denyAll             |   用户不能访问* fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)* hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问* hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问* hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问* hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问* hasRole             |   如果有参数,参数表示角色,则其角色可以访问* permitAll           |   用户可以任意访问* rememberMe          |   允许通过remember-me登录的用户访问* authenticated       |   用户登录后可访问*/@Beanprotected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{return httpSecurity// CSRF禁用,因为不使用session.csrf(csrf -> csrf.disable())// 禁用HTTP响应标头.headers((headersCustomizer) -> {headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());})// 认证失败处理类.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))// 基于token,所以不需要session.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 注解标记允许匿名访问的url.authorizeHttpRequests((requests) -> {permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());// 对于登录login 注册register 验证码captchaImage 允许匿名访问requests.antMatchers("/login", "/register", "/captchaImage").permitAll()// 静态资源,可匿名访问.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll().antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();})// 添加Logout filter.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))// 添加JWT filter.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)// 添加CORS filter.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class).addFilterBefore(corsFilter, LogoutFilter.class).build();}/*** 强散列哈希加密实现*/@Beanpublic BCryptPasswordEncoder bCryptPasswordEncoder(){return new BCryptPasswordEncoder();}
}

注意:身份认证实现,这里配置了hash加密,前端是传明文密码。数据库存储加密后的密码。加入前端直接传密文,会提示错误

        daoAuthenticationProvider.setUserDetailsService(userDetailsService);daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());

相关文章:

若依解析(一)登录认证流程

JWTSpringSecurity 6.X 实现登录 JWT token只包含uuid ,token 解析uuid&#xff0c;然后某个常量加UUID 从Redis缓存查询用户信息 流程图如下 感谢若依&#xff0c;感谢开源&#xff0c;能有这么好系统供我学习。 设计数据库&#xff0c;部门表&#xff0c;用户表&#xff0c…...

Redis设计与实现第17章 -- 集群 总结1(节点 槽指派)

集群通过分片sharding来进行数据共享&#xff0c;并提供复制和故障转移功能。 17.1 节点 一个Redis集群通常由多个节点node组成&#xff0c;刚开始每个节点都是相互独立的&#xff0c;必须将各个独立的节点连接起来&#xff0c;才能构成一个包含多个节点的集群。通过CLUSTER …...

汽车控制软件下载移动管家手机控车一键启动app

移动管家手机控制汽车系统是一款实现车辆远程智能控制的应用程序‌。通过下载并安装特定的APP&#xff0c;用户可以轻松实现以下功能&#xff1a;‌远程启动与熄火‌&#xff1a;无论身处何地&#xff0c;只要有网络&#xff0c;即可远程启动或熄火车辆&#xff0c;提前预冷或预…...

推荐几个可以免费下载网站模板的资源站

推荐几个可以免费下载网站模板的资源站&#xff0c;上面有免费的wordpress模板和帝国CMS模板可以下载。 模板帝 Mobandi.com 模板帝是一个提供丰富网站模板资源的平台&#xff0c;旨在帮助用户快速构建和美化自己的网站。无论是个人博客、企业官网还是电子商务平台&#xff…...

H3C OSPF实验

实验拓扑 实验需求 按照图示配置 IP 地址按照图示分区域配置 OSPF &#xff0c;实现全网互通为了路由结构稳定&#xff0c;要求路由器使用环回口作为 Router-id&#xff0c;ABR 的环回口宣告进骨干区域 实验解法 一、配置IP地址 [R1]int l0 [R1-LoopBack0]ip add 1.1.1.1 32 […...

Vue框架开发一个简单的购物车(Vue.js)

让我们利用所学知识来开发一个简单的购物车 &#xff08;记得暴露属性和方法&#xff01;&#xff01;&#xff01;&#xff09; 首先来看一下最基本的一个html框架 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"&…...

Windows Terminal Solarized Dark 配色方案调整

起因 Widnows 10/11 下面自带的 Terminal 还是比较方便的&#xff0c;因为不需要安装额外的 Terminal 软件。 我喜欢 Solarized Dark 配色方案&#xff0c;虽然有人批评这个配色方案比较老&#xff0c;但我觉得它比较优雅&#xff0c;尤其对外这种眼神比较差的人&#xff0c;比…...

PyTorch张量运算与自动微分

PyTorch张量运算与自动微分 PyTorch由Facebook人工智能研究院于2017年推出&#xff0c;具有强大的GPU加速张量计算功能&#xff0c;并且能够自动进行微分计算&#xff0c;从而可以使用基于梯度的方法对模型参数进行优化&#xff0c;大部分研究人员、公司机构、数据比赛都使用P…...

【从零开始的LeetCode-算法】3264. K 次乘运算后的最终数组 I

给你一个整数数组 nums &#xff0c;一个整数 k 和一个整数 multiplier 。 你需要对 nums 执行 k 次操作&#xff0c;每次操作中&#xff1a; 找到 nums 中的 最小 值 x &#xff0c;如果存在多个最小值&#xff0c;选择最 前面 的一个。将 x 替换为 x * multiplier 。 请你…...

【Linux】gdb / cgdb 调试 + 进度条

&#x1f33b;个人主页&#xff1a;路飞雪吖~ &#x1f320;专栏&#xff1a;Linux 目录 一、Linux调试器-gdb &#x1f31f;开始使用 &#x1f320;小贴士&#xff1a; &#x1f31f;gdb指令 &#x1f320;小贴士&#xff1a; ✨watch 监视 ✨打条件断点 二、小程序----进…...

Jenkins Nginx Vue项目自动化部署

目录 一、环境准备 1.1 Jenkins搭建 1.2 NVM和Nodejs安装 1.3 Nginx安装 二、Jenkins配置 2.1 相关插件安装 2.2 全局工具安装 2.3 环境变量配置 2.4 邮箱配置&#xff08;构建后发送邮件&#xff09; 2.5 任务配置 三、Nginx配置 3.1 配置路由转发 四、部署项目 …...

视频汇聚平台Liveweb国标GB28181视频平台监控中心设计

在现代安防视频监控领域&#xff0c;Liveweb视频汇聚平台以其卓越的兼容性和灵活的拓展能力&#xff0c;为用户提供了一套全面的解决方案。该平台不仅能够实现视频的远程监控、录像、存储与回放等基础功能&#xff0c;还涵盖了视频转码、视频快照、告警、云台控制、语音对讲以及…...

文件比较和文件流

文件比较和文件流 一、文本比较工具 diff1.基本用法1.1输出格式 2.常用选项 二、文件流1.文件的打开模式2.文件流的分类ifstreamofstreamfstrem区别 3.文件流的函数1. 构造函数2. is_open 用于判断文件是否打开3. open4. getline5. close6. get()7. read8. write9. put10. gcou…...

【2024最新】基于Springboot+Vue的就业信息管理系统Lw+PPT

作者&#xff1a;计算机搬砖家 开发技术&#xff1a;SpringBoot、php、Python、小程序、SSM、Vue、MySQL、JSP、ElementUI等&#xff0c;“文末源码”。 专栏推荐&#xff1a;SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;Java精选实战项…...

PySide6 QSS(Qt Style Sheets) Reference: PySide6 QSS参考指南

Qt官网参考资料&#xff1a; QSS介绍&#xff1a; Styling the Widgets Application - Qt for Pythonhttps://doc.qt.io/qtforpython-6/tutorials/basictutorial/widgetstyling.html#tutorial-widgetstyling QSS 参考手册&#xff1a; Qt Style Sheets Reference | Qt Widge…...

【笔记】成为雍正

观古代历史&#xff0c;不过帝王一家一姓之家史 时间 1662年&#xff0c;田文镜出生。1672年&#xff0c;张廷玉出生。1674年&#xff0c;胤礽出生。1678年&#xff0c;胤禛出生。1679年&#xff0c;年羹尧出生。1680年&#xff0c;鄂尔泰出生。1700年&#xff0c;索额图被赐死…...

Codeforces Round 913 (Div. 3)

题目链接 B. YetnotherrokenKeoard 题意 输入 输出 思路 用两个栈分别维护小写字母下标和大写字母下标&#xff0c;用一个vis数组标记字母是否删除 示例代码 void solve() {string s;cin >> s;int n s.size();vector<bool> vis(n, false);stack<int>sk…...

斐波那契数

C语言实现斐波那契数列的多种方法_斐波那契数列c语言-CSDN博客 题目描述 斐波那契数列为&#xff1a;1,1,2,3,5,8,13.....&#xff0c;常规递推公式f(n)f(n-1)f(n-2); 输入描述 输入一个整数n(0<n<50),为多组数据。 输出描述 输出第n个斐波那契数 样例输入 1 2 4样例输出…...

Redis高阶集群搭建+集群读写

问题 容量不够&#xff0c;redis 如何进行扩容&#xff1f;并发写操作&#xff0c; redis 如何分摊&#xff1f;另外&#xff0c;主从模式&#xff0c;薪火相传模式&#xff0c;主机宕机&#xff0c;导致 ip 地址发生变化&#xff0c;应用程序中配置需要修改对应的主机地址、端…...

Vision Transformer(vit)的主干

图解&#xff1a; 代码&#xff1a; class VisionTransformer(nn.Module):def __init__(self, img_size224, patch_size16, in_c3, num_classes1000,embed_dim768, depth12, num_heads12, mlp_ratio4.0, qkv_biasTrue,qk_scaleNone, representation_sizeNone, distilledFalse,…...

手撸了一个文件传输工具

在日常的开发与运维中&#xff0c;文件传输工具是不可或缺的利器。无论是跨服务器传递配置文件&#xff0c;还是快速从一台机器下载日志文件&#xff0c;一个高效、可靠且简单的文件传输工具能够显著提高工作效率。今天&#xff0c;我想分享我自己手撸一个文件传输工具的全过程…...

Java程序调kubernetes(k8s1.30.7)core API简单示例,并解决403权限验证问题,即何进行进行权限授权以及验证

简单记录问题 一、问题描述 希望通过Java程序使用Kubernetes提供的工具包实现对Kubernetes集群core API的调用&#xff0c;但是在高版本上遇见权限验证问题4xx。 <dependency><groupId>io.kubernetes</groupId><artifactId>client-java</artifact…...

java八股-Redis Stream和RocketMQ实现的解决方案

文章目录 Redis Stream方案&#xff1a;ShortLinkStatsSaveProducer.javaShortLinkStatsSaveConsumer.java RocketMQ方案ShortLinkStatsSaveProducer.javaShortLinkStatsSaveConsumer.java Redis Stream方案&#xff1a; ShortLinkStatsSaveProducer.java package com.nageoff…...

第29天 MCU入门

目录 MCU介绍 MCU的组成与作用 电子产品项目开发流程 硬件开发流程 常用元器件初步了解 硬件原理图与PCB板 常见电源符号和名称 电阻 电阻的分类 贴片电阻的封装说明&#xff1a; 色环电阻的计算 贴片电阻阻值计算 上拉电阻与下拉电阻 电容 电容的读数 二极管 LED 灯电路 钳位作…...

【Python网络爬虫笔记】6- 网络爬虫中的Requests库

一、概述 Requests 是一个用 Python 语言编写的、简洁且功能强大的 HTTP 库。它允许开发者方便地发送各种 HTTP 请求&#xff0c;如 GET、POST、PUT、DELETE 等&#xff0c;并且可以轻松地处理请求的响应。这个库在 Python 生态系统中被广泛使用&#xff0c;无论是简单的网页数…...

Linux网络_网络协议_网络传输_网络字节序

一.协议 1.概念 协议&#xff08;Protocol&#xff09; 是一组规则和约定&#xff0c;用于定义计算机网络中不同设备之间如何进行通信和数据交换。协议规定了数据的格式、传输方式、传输顺序等详细规则&#xff0c;确保不同设备和系统能够有效地互联互通。 在网络通信中&#…...

浅谈网络 | 应用层之流媒体与P2P协议

目录 流媒体名词系列视频的本质视频压缩编码过程如何在直播中看到帅哥美女&#xff1f;RTMP 协议 P2PP2P 文件下载种子文件 (.torrent)去中心化网络&#xff08;DHT&#xff09;哈希值与 DHT 网络DHT 网络是如何查找 流媒体 直播系统组成与协议 近几年直播比较火&#xff0c;…...

css vue vxe-text-ellipsis table 实现多行文本超出隐藏省略

分享 vxe-text-ellipsis table grid 多行文本溢出省略的用法 正常情况下如果需要使用文本超出隐藏&#xff0c;通过 css 就可以完成 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;但是如果需要实现多行文本溢出&#xff0c;就很难实现里&#xff0c;谷歌…...

基于hexo框架的博客搭建流程

这篇博文讲一讲hexo博客的搭建及文章管理&#xff0c;也算是我对于暑假的一个交代 &#xff01;&#xff01;&#xff01;注意&#xff1a;下面的操作是基于你已经安装了node.js和git的前提下进行的&#xff0c;并且拥有github账号 创建一个blog目录 在磁盘任意位置创建一个…...

数据结构-简单排序

一.前提 二.冒泡排序 三.插入排序 #include<iostream> using namespace std; typedef int ElemengType; void Bubble_Sort(ElemengType A[], int N) {for (int p N - 1; p > 0; p--) {int flag 0;for (int i 0; i < p; i) {if (A[i] > A[i 1]) {swap(A[i], …...

上海网站建设公司兴田德润优惠吗/百度竞价是什么意思

1、函数定义的弊端&#xff1a; Python是动态语言&#xff0c;变量随时可以被赋值&#xff0c;且能赋值为不同的类型。 Python不是静态编译型语言&#xff0c;变量类型是在运行器决定的 动态语言很灵活&#xff0c;但是这种特性也是弊端&#xff1a; 1 def add(x, y): 2 r…...

30人的网站建设公司年利润是多少/推广普通话手抄报

Matlab期末复习2008.06.04第 1章 MATLAB语言概述第 2章 基本语法第 4章 Matlab的其它函数库第 6章 Matlab在信号与系统中的应用第 9章 Matlab工具箱简介第 1章 MATLAB语言概述了解 MATLAB的基本知识熟悉 MATLAB的上机环境Matlab的工作环境1、命令窗2、图形窗3、文本编辑窗ESC键…...

深圳大浪网站建设/互联网营销师题库

一、下载对应的jar包 下载地址&#xff1a;zookeeper jdk 通过ftp传输方式 通过wget指令 wget http://mirrors.hust.edu.cn/apache/zookeeper/zookeeper-3.5.8/apache-zookeeper-3.5.8-bin.tar.gz二、创建zookeeper用户登录 useradd zookeeper passwd zookeeper接着键入你的…...

成都哪里有做网站的/同城广告发布平台

在pycharm导入 import threading直接打印 print(threading.active_count()) 结果是1, 因为当前只有一个主线程, 那些已经死去的线程和实例化了线程对象, 但是没有start()启动的线程不会被计算在内.threading.active_count() 显示的线程数量和 len(threading.enumerate()) 的返回…...

网站备案后内容/合肥网站推广公司排名

一、小序 数据结构中&#xff0c;提供针对某些特殊矩阵的压缩存储结构。 这里所说的特殊矩阵&#xff0c;主要分为以下两类&#xff1a; 含有大量相同数据元素的矩阵&#xff0c;比如对称矩阵&#xff1b;含有大量 0 元素的矩阵&#xff0c;比如稀疏矩阵、上&#xff08;下&am…...

常州制作网站价格/运营和营销的区别和联系

1 定义或者声明方法int method(char *arg1,char* arg2...);实现方法int method(char *arg1,char* arg2...)EXEC SQL BEGIN DECLARE SECTION;char *arg1;char* arg2;EXEC SQL END DECLARE SECTION;{EXEC SQL BEGIN DECLARE SECTION;定义自己的变量。仅仅有此处定义的局部变量能够…...