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

Springboot3.x.x使用SpringSecurity6(一文包搞定)

SpringSecurity6

什么是SpringSecurity?

Spring Security 是一个强大的、高度可定制的身份验证(Authentication)和访问控制(Authorization)框架。它是 Spring 框架家族的一员,主要用于保护基于 Java 的应用程序,无论是Web应用还是非Web应用。Spring Security 提供了以下功能:

  • 认证:管理用户凭证的验证过程,确定用户是否可以登录到系统。

  • 授权:控制经过认证的用户能够访问哪些资源或执行哪些操作。

  • 会话管理:对于Web应用,Spring Security 还处理用户的会话。

  • 跨站请求伪造(CSRF)保护:防止恶意网站利用用户的登录状态执行不受信任的操作。

  • 点击劫持保护:通过HTTP头部设置来帮助防御点击劫持攻击。

  • 加密和编码支持:提供密码加密和其他安全相关的编码任务。

    Spring Security 可以与 Spring MVC 和 Spring WebFlux 紧密集成,同时也支持传统的 Servlet API。它允许开发者以声明式的方式定义安全约束,并且可以通过编程方式自定义安全策略。此外,Spring Security 还支持多种认证方式,如表单登录、HTTP基本认证、OAuth2、OpenID Connect 等。

    在过去,Spring Security 的配置相对复杂,但是随着 Spring Boot 的出现,它提供了自动配置方案,使得集成 Spring Security 变得更为简单,甚至可以做到“零配置”使用。这使得 Spring Security 在现代 Java 应用程序的安全性管理方面变得非常流行。

Spring Security实现权限

要对Web资源进行保护,最好的办法莫过于Filter 要想对方法调用进行保护,最好的办法莫过于AOP。

Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter来进行拦截的。

img

如图所示,一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。

这里面我们只需要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter负责登录认证,FilterSecurityInterceptor负责权限授权。

说明:Spring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!这个框架的使用方式就是对这些过滤器和组件进行扩展。

用户认证流程

认证核心

我们系统中会有许多用户,确认当前是哪个用户正在使用我们系统就是登录认证的最终目的。这里我们就提取出了一个核心概念:当前登录用户/当前认证用户。整个系统安全都是围绕当前登录用户展开的,这个不难理解,要是当前登录用户都不能确认了,那A下了一个订单,下到了B的账户上这不就乱套了。这一概念在Spring Security中的体现就是 Authentication,它存储了认证信息,代表当前登录用户。

我们在程序中如何获取并使用它呢?我们需要通过 SecurityContext 来获取AuthenticationSecurityContext就是我们的上下文对象!这个上下文对象则是交由 SecurityContextHolder 进行管理,你可以在程序任何地方使用它:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

SecurityContextHolder原理非常简单,就是使用ThreadLocal来保证一个线程中传递同一个对象!

现在我们已经知道了Spring Security中三个核心组件:

1、Authentication:存储了认证信息,代表当前登录用户

2、SeucirtyContext:上下文对象,用来获取Authentication

3、SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext

Authentication中是什么信息呢:

1、Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象

2、Credentials:用户凭证,一般是密码

3、Authorities:用户权限

认证接口

AuthenticationManager的校验逻辑非常简单:

根据用户名先查询出用户对象(没有查到则抛出异常)将用户对象的密码和传递过来的密码进行校验,密码不匹配则抛出异常。

这个逻辑没啥好说的,再简单不过了。重点是这里每一个步骤Spring Security都提供了组件:

1、是谁执行 根据用户名查询出用户对象 逻辑的呢?用户对象数据可以存在内存中、文件中、数据库中,你得确定好怎么查才行。这一部分就是交由UserDetialsService 处理,该接口只有一个方法loadUserByUsername(String username),通过用户名查询用户对象,默认实现是在内存中查询。

2、那查询出来的 用户对象 又是什么呢?每个系统中的用户对象数据都不尽相同,咱们需要确认我们的用户数据是啥样的才行。Spring Security中的用户数据则是由UserDetails 来体现,该接口中提供了账号、密码等通用属性。

3、对密码进行校验大家可能会觉得比较简单,if、else搞定,就没必要用什么组件了吧?但框架毕竟是框架考虑的比较周全,除了if、else外还解决了密码加密的问题,这个组件就是PasswordEncoder,负责密码加密与校验。

我们可以看下AuthenticationManager校验逻辑的大概源码:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...省略其他代码// 传递过来的用户名String username = authentication.getName();// 调用UserDetailService的方法,通过用户名查询出用户对象UserDetail(查询不出来UserDetailService则会抛出异常)UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username);String presentedPassword = authentication.getCredentials().toString();// 传递过来的密码String password = authentication.getCredentials().toString();// 使用密码解析器PasswordEncoder传递过来的密码是否和真实的用户密码匹配if (!passwordEncoder.matches(password, userDetails.getPassword())) {// 密码错误则抛出异常throw new BadCredentialsException("错误信息...");}// 注意哦,这里返回的已认证Authentication,是将整个UserDetails放进去充当PrincipalUsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,authentication.getCredentials(), userDetails.getAuthorities());return result;...省略其他代码
}

UserDetialsServiceUserDetailsPasswordEncoder,这三个组件Spring Security都有默认实现,这一般是满足不了我们的实际需求的,所以这里我们自己来实现这些组件!

加密器PasswordEncoder

加密我们项目采取MD5加密

操作模块:spring-security模块

自定义加密处理组件:CustomMd5PasswordEncoder

package com.atguigu.system.custom;import com.atguigu.common.util.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;/*** <p>* 密码处理* </p>**/
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {public String encode(CharSequence rawPassword) {return MD5.encrypt(rawPassword.toString());}public boolean matches(CharSequence rawPassword, String encodedPassword) {return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));}
}
用户对象UserDetails

该接口就是我们所说的用户对象,它提供了用户的一些通用属性,源码如下:

 

public interface UserDetails extends Serializable {/*** 用户权限集合(这个权限对象现在不管它,到权限时我会讲解)*/Collection<? extends GrantedAuthority> getAuthorities();/*** 用户密码*/String getPassword();/*** 用户名*/String getUsername();/*** 用户没过期返回true,反之则false*/boolean isAccountNonExpired();/*** 用户没锁定返回true,反之则false*/boolean isAccountNonLocked();/*** 用户凭据(通常为密码)没过期返回true,反之则false*/boolean isCredentialsNonExpired();/*** 用户是启用状态返回true,反之则false*/boolean isEnabled();
}

实际开发中我们的用户属性各种各样,这些默认属性可能是满足不了,所以我们一般会自己实现该接口,然后设置好我们实际的用户实体对象。实现此接口要重写很多方法比较麻烦,我们可以继承Spring Security提供的org.springframework.security.core.userdetails.User类,该类实现了UserDetails接口帮我们省去了重写方法的工作。

了解以上后我们即可进入使用了

 

表结构

 

#用户表
CREATE TABLE `sys_user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户主键',`wx_openid` varchar(100) DEFAULT NULL COMMENT '微信的Openid',`session_key` varchar(100) DEFAULT NULL COMMENT '微信的sessionKey(选择存储)',`phone` varchar(20) DEFAULT NULL COMMENT '手机号',`sex` char(1) DEFAULT NULL COMMENT '性别',`username` varchar(35) DEFAULT NULL COMMENT '用户名称',`vx_avatar` varchar(255) DEFAULT NULL COMMENT '微信头像路径',`status` tinyint(1) DEFAULT NULL COMMENT '是否可用 0可用 1不可用,默认0',`pwd` varchar(255) DEFAULT NULL COMMENT '密码',`type` tinyint(1) NOT NULL COMMENT '0用户登录,1管理员',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标记(0:未删除,1:删除)',`gmt_founder` varchar(35) DEFAULT NULL COMMENT '创建人',PRIMARY KEY (`id`),UNIQUE KEY `wx_openid` (`wx_openid`),UNIQUE KEY `phone` (`phone`),UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=58 DEFAULT CHARSET=utf8mb3 COMMENT='用户表';#用户角色表
CREATE TABLE `sys_user_role` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户角色主键',`role_id` bigint NOT NULL COMMENT '角色主键',`user_id` bigint NOT NULL COMMENT '用户主键',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`is_deleted` tinyint(1) DEFAULT '0' COMMENT '删除标记(0:未删除,1:删除)',`gmt_founder` varchar(35) DEFAULT NULL COMMENT '创建人',PRIMARY KEY (`id`),KEY `id_role_id` (`role_id`) USING BTREE,KEY `id_user_id` (`user_id`) USING BTREE,CONSTRAINT `sys_user_role_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`),CONSTRAINT `sys_user_role_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3 COMMENT='用户角色表';#角色表
CREATE TABLE `sys_role` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色主键',`role_name` varchar(20) NOT NULL COMMENT '角色名称',`role_code` varchar(20) DEFAULT NULL COMMENT '角色编码',`description` varchar(100) DEFAULT NULL COMMENT '角色描述',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`is_deleted` tinyint(1) DEFAULT '0' COMMENT '删除标记(0:未删除,1:删除)',`gmt_founder` varchar(35) DEFAULT NULL COMMENT '创建人',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb3 COMMENT='角色管理表';#菜单表
CREATE TABLE `sys_menu` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单主键',`parent_id` bigint NOT NULL COMMENT '所属上级菜单',`name` varchar(20) NOT NULL COMMENT '菜单名字',`type` tinyint NOT NULL COMMENT '菜单类型(0:目录,1:菜单,2:按钮)',`path` varchar(100) DEFAULT NULL COMMENT '路由地址',`component` varchar(100) DEFAULT NULL COMMENT '组件路径',`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',`icon` varchar(100) DEFAULT NULL COMMENT '菜单图标',`sort_value` int DEFAULT NULL COMMENT '菜单排序',`status` tinyint DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`is_deleted` tinyint(1) DEFAULT '0' COMMENT '删除标记(0:未删除,1:删除)',`always_show` tinyint unsigned DEFAULT NULL COMMENT '总是展示(0:不展示,1展示)',`hidden` tinyint(1) DEFAULT NULL COMMENT '是否展示(0:不展示,1展示)',`keep_alive` tinyint(1) DEFAULT NULL COMMENT '是否缓存(0:不缓存,1缓存)',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3 COMMENT='菜单表';#角色菜单关联表
CREATE TABLE `sys_role_menu` (`id` bigint NOT NULL AUTO_INCREMENT,`role_id` bigint NOT NULL DEFAULT '0',`menu_id` bigint NOT NULL DEFAULT '0',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记(0:可用 1:已删除)',PRIMARY KEY (`id`),KEY `id_role_id` (`role_id`) USING BTREE,KEY `id_menu_id` (`menu_id`) USING BTREE,CONSTRAINT `sys_role_menu_ibfk_1` FOREIGN KEY (`menu_id`) REFERENCES `sys_menu` (`id`),CONSTRAINT `sys_role_menu_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=426 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='角色菜单';

表关系

 

 

首先要明白表之间的关系,了解下上面表的字段!!!准备工作完成后即可进入代码环节了。

Spring Security6的用户认证

这里我Springboot的版本是3.x.x

<dependency><groupId>jakarta.servlet</groupId><artifactId>jakarta.servlet-api</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>${jjwt.version}</version></dependency><!-- 如果jdk大于1.8,则还需导入下面依赖--><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>${jaxb.version}</version></dependency><!--    SpringSecurity依赖    --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--    JWT依赖    --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-impl</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-core</artifactId><version>2.3.0</version></dependency>

 

导入依赖后无法启动时正常的,因为没有配置文件~~~

流程:

1.我们先将账号密码交于UsernamePasswordAuthenticationToken

2.随后配置security

3.关联数据库获取UserDetail

4.编写认证监听器、过滤器等等

编写登录接口

controller

@RestController
@Tag(name = "登录接口/认证")
@RequestMapping("/api/v1/auth")
public class LoginController {@Resourceprivate UserService userService;@Resourceprivate RedisTemplate<String, String> redisTemplate;/** @param loginDto* @return*/@Operation(summary = "账号密码登录接口")@PostMapping("/login")public Result login(@RequestBody LoginDto loginDto){return userService.login(loginDto);}
}

注意这里的路径我们是自定义登录接口所以路径为:/api/v1/auth/login  

IMPL  

@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate SmsUtils smsUtils;@Autowiredprivate MenuService menuService;private final AuthenticationManager authenticationManager;@Overridepublic Result login(LoginDto loginDto) {if (StringUtils.isBlank(loginDto.getUsername()) && StringUtils.isBlank(loginDto.getPassword())) {return Result.fail(500, "用户名或密码不能为空~");}UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());Authentication authentication = authenticationManager.authenticate(authenticationToken);String accessToken = JwtUtils.generateToken(authentication);LoginVo loginVO = new LoginVo().setAccessToken(accessToken).setTokenType("Bearer");return Result.success(loginVO);}
}

 解释:我们上面就把账号密码交于UsernamePasswordAuthenticationToken去处理了

 

 jwt工具类

 

/*** JWT 工具类** @author debug*/
@Component
public class JwtUtils {/*** JWT 加解密使用的密钥*/private static byte[] key;/*** JWT Token 的有效时间(单位:秒)*/private static int ttl;/*** 生成 JWT Token** @param authentication 用户认证信息* @return Token 字符串*/public static String generateToken(Authentication authentication) {SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();Map<String, Object> payload = new HashMap<>();payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID// claims 中添加角色信息Set<String> roles = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());payload.put(JwtClaimConstants.AUTHORITIES, roles);Date now = new Date();Date expiration = DateUtil.offsetSecond(now, ttl);payload.put(JWTPayload.ISSUED_AT, now);payload.put(JWTPayload.EXPIRES_AT, expiration);payload.put(JWTPayload.SUBJECT, authentication.getName());payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());return JWTUtil.createToken(payload, JwtUtils.key);}/*** 从 JWT Token 中解析 Authentication  用户认证信息** @param payload JWT 载体* @return 用户认证信息*/public static UsernamePasswordAuthenticationToken getAuthentication(Map<String, Object> payload) {SysUserDetails userDetails = new SysUserDetails();// 用户IDuserDetails.setUserId(Convert.toLong(payload.get(JwtClaimConstants.USER_ID)));// 用户名userDetails.setUsername(Convert.toStr(payload.get(JWTPayload.SUBJECT)));// 角色集合Set<SimpleGrantedAuthority> authorities = ((JSONArray) payload.get(JwtClaimConstants.AUTHORITIES)).stream().map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))).collect(Collectors.toSet());return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);}/*** 解析 JWT Token 获取载体信息** @param token JWT Token* @return 载体信息*/public static Map<String, Object> parseToken(String token) {try {if (StrUtil.isBlank(token)) {return null;}if (token.startsWith("Bearer ")) {token = token.substring(7);}JWT jwt = JWTUtil.parseToken(token);if (jwt.setKey(JwtUtils.key).validate(0)) {return jwt.getPayloads();}} catch (Exception ignored) {}return null;}@Value("${jwt.key}")public void setKey(String key) {JwtUtils.key = key.getBytes();}@Value("${jwt.ttl}")public void setTtl(Integer ttl) {JwtUtils.ttl = ttl;}
}
# 认证配置
jwt:# 密钥key: SecretKey012345678901234567890123456789012345678901234567890123456789# token 过期时间(单位:秒)ttl: 7200
//JwtClaimConstantspublic interface JwtClaimConstants {/*** 用户ID*/String USER_ID = "userId";/*** 权限(角色Code)集合*/String AUTHORITIES = "authorities";
}
绑定管理数据库获取UserDetail

/*** Spring Security 用户对象** @author debug*/
@Data
@NoArgsConstructor
public class SysUserDetails implements UserDetails {private Long userId;private String username;private String phone;private String password;//    private Boolean enabled;private Integer status;private Collection<SimpleGrantedAuthority> authorities;//权限信息@TableField(exist = false)private Set<String> perms;@TableField(exist = false)private Set<String> roles;private Boolean enabled;//数据范围private Integer dataScope;public SysUserDetails(UserAuthInfo user) {this.userId = user.getUserId();this.roles=user.getRoles();Set<String> roles = user.getRoles();Set<SimpleGrantedAuthority> authorities;if (CollectionUtil.isNotEmpty(roles)) {authorities = roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // 标识角色.collect(Collectors.toSet());} else {authorities = Collections.EMPTY_SET;}this.authorities = authorities;this.username = user.getUsername();this.password = user.getPassword();this.enabled = ObjectUtil.equal(user.getStatus(), 0);this.perms = user.getPerms();}public Long getUserId() {return this.userId;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return this.enabled;}
}

 通过实现UserDetailService的loadUserByUsername方法获取数据库里面的用户数据等。

/*** 系统用户认证* @author debug*/
@Service
@RequiredArgsConstructor
public class SysUserDetailsService implements UserDetailsService {private final UserMapper userMapper;private final MenuService menuService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {UserAuthInfo userAuthInfo = this.userMapper.getUserAuthInfo(username);if (userAuthInfo == null) {throw new UsernameNotFoundException(username);}Set<String> roles = userAuthInfo.getRoles();if (CollectionUtil.isNotEmpty(roles)) {Set<String> perms = menuService.listRolePerms(roles);userAuthInfo.setPerms(perms);}return new SysUserDetails(userAuthInfo);}
}

 

getUserAuthInfo()  

listRolePerms()  

 

 <select id="listRolePerms" resultType="java.lang.String">SELECTDISTINCT t1.permsFROMsys_menu t1INNER JOIN sys_role_menu t2 ON t1.id = t2.menu_idINNER JOIN sys_role t3 ON t3.id = t2.role_idAND t1.type = 2AND t1.perms IS NOT NULL<choose><when test="roles!=null and roles.size()>0">AND t3.role_code IN<foreach collection="roles" item="role" separator="," open="(" close=")">#{role}</foreach></when><otherwise>AND t1.id = -1</otherwise></choose></select>

 

 

Security配置类
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {// 自定义未认证处理类private final MyAuthenticationEntryPoint authenticationEntryPoint;// 自定义无权限访问处理类@Resourceprivate final MyAccessDeniedHandler accessDeniedHandler;// Redis操作模板@Autowiredprivate final RedisTemplate<String, Object> redisTemplate;/*** 配置Spring Security过滤器链。** @param http HttpSecurity对象,用于构建安全配置* @return 构建好的SecurityFilterChain对象* @throws Exception 配置过程中可能抛出的异常*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(requestMatcherRegistry ->// 配置请求授权规则//登录路径公开访问requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH,SecurityConstants.LOGOUT_PATH,SecurityConstants.VERIFY_TREE_PATH,SecurityConstants.GET_PHONE_CODE_PATH,SecurityConstants.PHONE_LOGIN_PATH).permitAll()// 其他所有请求都需要认证.anyRequest().authenticated())// 禁用Session创建.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 配置异常处理.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->httpSecurityExceptionHandlingConfigurer// 设置未认证处理入口.authenticationEntryPoint(authenticationEntryPoint)// 设置无权限访问处理.accessDeniedHandler(accessDeniedHandler))// 禁用CSRF保护.csrf(AbstractHttpConfigurer::disable);// JWT 校验过滤器http.addFilterBefore(new JwtValidationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class);// 构建并返回过滤器链return http.build();}/*** 不走过滤器链的放行配置*/@Beanpublic WebSecurityCustomizer webSecurityCustomizer() {// 忽略指定路径的安全检查return (web) -> web.ignoring().requestMatchers("/api/v1/auth/captcha","/webjars/**","/doc.html","/swagger-resources/**","/v3/api-docs/**","/swagger-ui/**","/swagger-ui.html","/ws/**","/ws-app/**");}/*** 密码编码器*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 手动注入AuthenticationManager,用于处理认证和授权请求。** @param authenticationConfiguration 认证配置对象* @return AuthenticationManager对象* @throws Exception 配置过程中可能抛出的异常*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {// 获取认证管理器实例return authenticationConfiguration.getAuthenticationManager();}
}

放开接口类SecurityConstants


public interface SecurityConstants {/*** 登录接口路径*/String LOGIN_PATH = "/api/v1/auth/login";/***  验证码接口路径*/String VERIFY_TREE_PATH = "/api/v1/auth/getVerifyThree";/*** 退出登录接口*/String  LOGOUT_PATH = "/api/v1/auth/logout";/*** 手机号登录接口*/String PHONE_LOGIN_PATH = "/api/v1/auth/phoneLogin";/*** 获取手机验证码*/String GET_PHONE_CODE_PATH = "/api/v1/auth/sendCode";
}
jwt校验过滤器

@Slf4j
public class JwtValidationFilter extends OncePerRequestFilter {private final RedisTemplate<String, Object> redisTemplate;/*** 构造函数* @param redisTemplate Redis模板,用于操作Redis*/public JwtValidationFilter(RedisTemplate<String, Object> redisTemplate) {this.redisTemplate = redisTemplate;}/*** 从请求中获取 JWT Token,校验 JWT Token 是否合法* <p>* 如果合法则将 Authentication 设置到 Spring Security Context 上下文中* 如果不合法则清空 Spring Security Context 上下文,并直接返回响应*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//从请求头中提取TokenString token = request.getHeader(HttpHeaders.AUTHORIZATION);try {//如果Token非空,则进行解析if (StrUtil.isNotBlank(token)) {//解析Token的Payload部分Map<String, Object> payload = JwtUtils.parseToken(token);String jti = null;//如果Payload非空,提取JWT IDif (payload != null) {jti = Convert.toStr(payload.get(JWTPayload.JWT_ID));}//从Payload中获取认证信息Authentication authentication = JwtUtils.getAuthentication(payload);//将认证信息设置到Spring Security上下文中SecurityContextHolder.getContext().setAuthentication(authentication);}} catch (CustomException ex) {log.error("拦截出现错误,错误码为:{}", ex.getCode());ex.printStackTrace();//this is very important, since it guarantees the user is not authenticated at all//如果解析过程中出现业务异常,清除Security上下文并返回错误响应SecurityContextHolder.clearContext();ResponseUtils.writeErrMsg(response, ex.getCode());return;}//继续请求链filterChain.doFilter(request, response);}
}

 每个请求都会先进该过滤器

认证异常处理类

/*** 认证异常处理* 当未认证的用户尝试访问需要认证的资源时,该类负责处理相关的认证异常* 并向客户端返回具体的错误信息*/
@Component
@Slf4j
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {/*** 开始处理认证异常** @param request   当前的HTTP请求* @param response  当前的HTTP响应* @param authException  引发的认证异常* @throws IOException       如果在处理过程中发生输入输出异常* @throws ServletException 如果在处理过程中发生Servlet异常*/@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {// 获取当前HTTP响应的状态码int status = response.getStatus();// 判断HTTP状态码是否为未找到资源(404)if (status == HttpServletResponse.SC_NOT_FOUND) {// 资源不存在,向客户端返回自定义的资源未找到错误信息ResponseUtils.writeErrMsg(response, ResultEnum.RESOURCE_NOT_FOUND);} else {// 判断引发的认证异常是否为凭证无效异常(例如用户名或密码错误)if(authException instanceof BadCredentialsException){// 用户名或密码错误,向客户端返回自定义的用户名或密码错误信息ResponseUtils.writeErrMsg(response, ResultEnum.ARGUMENT_VALID_ERROR);} else {// 处理其他类型的认证异常,如未认证或者令牌(token)过期// 向客户端返回自定义的令牌无效错误信息ResponseUtils.writeErrMsg(response, ResultEnum.TOKEN_INVALID);}}}
}
Security访问异常处理器  
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {//访问没有授权ResponseUtils.writeErrMsg(response, ResultEnum.ACCESS_UNAUTHORIZED);}
}

 ResultEnum类

package com.brush.brushcommon.enums;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
/*** @ClassName: ResultEnum* @Description:* @Author: cws* @Date: 2023/1/5 16:43*/
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum ResultEnum {ENUM_USERNAME_NULL(6666,"账号不正确或者没有此用户哦~"),SUCCESS(200,"成功"),FAIL(201, "失败"),SERVICE_ERROR(2012, "服务异常"),DATA_ERROR(204, "数据异常"),ILLEGAL_REQUEST(205, "非法请求"),REPEAT_SUBMIT(206, "重复提交"),ARGUMENT_VALID_ERROR(210, "参数校验异常"),LOGIN_AUTH(208, "未登陆"),PERMISSION(209, "没有权限"),ACCOUNT_ERROR(214, "账号不正确"),PASSWORD_ERROR(215, "密码不正确"),LOGIN_MOBLE_ERROR( 216, "账号不正确"),ACCOUNT_STOP( 217, "账号已停用"),NODE_ERROR( 218, "该节点下有子节点,不可以删除"),TOKEN_INVALID(230, "token无效或已过期"),TOKEN_ACCESS_FORBIDDEN(231, "token已被禁止访问"),ACCESS_UNAUTHORIZED(301, "访问未授权"),RESOURCE_NOT_FOUND(401, "请求资源不存在"),PARAM_ERROR(400, "用户请求参数错误"),;private int code;private String msg;
}

ResponseUtils工具  

 


public class ResponseUtils {/*** 异常消息返回方法,针对不同类型的错误设置适当的HTTP状态码* 并以JSON格式向客户端返回错误信息** @param response  HttpServletResponse对象,用于获取响应输出流并设置响应头信息* @param resultEnum 结果枚举,表示不同的错误类型,用于确定响应的状态码和消息体内容* @throws IOException 如果在写入响应时发生I/O错误*/public static void writeErrMsg(HttpServletResponse response, ResultEnum resultEnum) throws IOException {// 根据不同的结果枚举设置相应的HTTP状态码switch (resultEnum) {case ACCESS_UNAUTHORIZED:case TOKEN_INVALID:response.setStatus(HttpStatus.UNAUTHORIZED.value());break;case TOKEN_ACCESS_FORBIDDEN:response.setStatus(HttpStatus.FORBIDDEN.value());break;default:response.setStatus(HttpStatus.BAD_REQUEST.value());break;}// 设置响应内容类型为JSONresponse.setContentType(MediaType.APPLICATION_JSON_VALUE);// 设置字符编码,确保响应内容的正确显示response.setCharacterEncoding("UTF-8");// 将错误信息结果转换为JSON字符串并写入响应//TODO:这里强转了,不知道会不会错。response.getWriter().print(JSONUtil.toJsonStr(Result.fail(resultEnum.toString())));}public static void writeErrMsg(HttpServletResponse response, Integer resultEnum) throws IOException {// 根据不同的结果枚举设置相应的HTTP状态码switch (resultEnum) {case 301:case 230:response.setStatus(HttpStatus.UNAUTHORIZED.value());break;case 231:response.setStatus(HttpStatus.FORBIDDEN.value());break;default:response.setStatus(HttpStatus.BAD_REQUEST.value());break;}// 设置响应内容类型为JSONresponse.setContentType(MediaType.APPLICATION_JSON_VALUE);// 设置字符编码,确保响应内容的正确显示response.setCharacterEncoding("UTF-8");// 将错误信息结果转换为JSON字符串并写入响应//TODO:这里强转了,不知道会不会错。response.getWriter().print(JSONUtil.toJsonStr(Result.fail(resultEnum.toString())));}
}

 最后一步就是编写自定义异常处理了

自定义异常
@AllArgsConstructor
@NoArgsConstructor
@Data
public class CustomException  extends RuntimeException{private Integer code;private String msg;
}

 


@ControllerAdvice
//顾名思义,@ControllerAdvice就是@Controller 的增强版。@ControllerAdvice主要用来处理全局数据,一般搭配@ExceptionHandler、@ModelAttribute以及@InitBinder使用。
@Slf4j
public class AllExceptionHandler {//进行异常处理,处理Exception.class的异常@ExceptionHandler(Exception.class)@ResponseBody //返回json数据如果不加就返回页面了public Result doException(Exception ex) {//e.printStackTrace();是打印异常的堆栈信息,指明错误原因,// 其实当发生异常时,通常要处理异常,这是编程的好习惯,所以e.printStackTrace()可以方便你调试程序!ex.printStackTrace();System.out.println(ex.getClass());System.out.println(ex.getMessage());log.error("出现异常:{}",ex.getClass()+":"+ex.getMessage());return Result.fail(9999,ex.getMessage());}//自定义异常@ExceptionHandler(CustomException.class)@ResponseBody //返回json数据如果不加就返回页面了public Result CustomException(CustomException ex) {//e.printStackTrace();是打印异常的堆栈信息,指明错误原因,// 其实当发生异常时,通常要处理异常,这是编程的好习惯,所以e.printStackTrace()可以方便你调试程序!ex.printStackTrace();//自定义的code和msglog.error("出现异常:{}",ex.getClass()+":"+ex.getMessage());return Result.fail(ex.getCode(),ex.getMsg());}/*** 参数不能为空*/@ExceptionHandler(MissingServletRequestParameterException.class)@ResponseBodypublic Result bindException(MissingServletRequestParameterException exception) {log.error("出现异常:{}",exception.getClass()+":"+exception.getMessage());return Result.fail(400, String.format("参数%s不能为空!", exception.getParameterName()));}/*** boby参数为空异常* @param exception* @return*/@ExceptionHandler(HttpMessageNotReadableException.class)@ResponseBodypublic Result bindException(HttpMessageNotReadableException exception) {log.error("出现异常:{}",exception.getClass()+":"+exception.getMessage());return Result.fail(400, "body参数不能为空!");}/*** AuthorizationDeniedException 没有权限访问*/@ExceptionHandler(AuthorizationDeniedException.class)@ResponseBodypublic Result bindException(AuthorizationDeniedException exception) {log.error("出现异常:{}",exception.getClass()+":"+exception.getMessage());return Result.fail(403, "您没有权限访问该接口!");}/*** 缺少参数异常* @param exception* @return*/@ExceptionHandler(MissingRequestHeaderException.class)@ResponseBodypublic Result bindException(MissingRequestHeaderException exception) {log.error("出现异常:{}",exception.getClass()+":"+exception.getMessage());return Result.fail(400, String.format("参数%s不能为空!",  exception.getHeaderName()));}@ExceptionHandler(BindException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public Result processException(BindException e) {log.error("BindException:{}", e.getMessage());String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));return Result.fail(ResultEnum.PARAM_ERROR.getCode(), msg);}/*** 请求方式异常* @param exception* @return*/@ExceptionHandler(HttpRequestMethodNotSupportedException.class)@ResponseBodypublic Result bindException(HttpRequestMethodNotSupportedException exception) {log.error("出现异常:{}",exception.getClass()+":"+exception.getMessage());return Result.fail(400, String.format("请求方式异常",  exception.getMessage()));}@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)@ExceptionHandler({SQLException.class})@ResponseBodypublic Result handleSQLException(SQLException exception) {log.error("出现异常:{}",exception.getClass()+":"+exception.getMessage());return Result.fail(400, String.format("服务运行SQLException异常",  exception.getMessage()));}/*** 校验参数异常* @param exception* @return*/@ExceptionHandler(ValidationException.class)@ResponseBodypublic Result bindException(ValidationException exception) {if(exception instanceof ConstraintViolationException) {return Result.fail(400, String.format("参数%s不能为空!", ((ConstraintViolationException) exception).getConstraintViolations()));}log.error("出现异常:{}",exception.getClass()+":"+exception.getMessage());return Result.fail(400, String.format("参数%s不能为空!", exception.getCause()));}/*** 数据库异常* @param* @return*/@ExceptionHandler(value = DataAccessException.class)@ResponseBodypublic Result repeatException(SQLIntegrityConstraintViolationException exception) {log.error("出现异常:{}",exception.getClass()+":"+exception.getMessage());return Result.fail(999, exception.getMessage());}/*** BuilderException  mybatis sql 构建异常*/@ExceptionHandler(value = RuntimeException.class)@ResponseBodypublic Result repeatException(RuntimeException exception) {log.error("出现异常:{}",exception.getClass()+":"+exception.getMessage());return Result.fail(999, exception.getMessage());}
}
测试

 

带token即可返回成功!!!  

 

用户授权

在这之前我们需要了解一个注解@PreAuthorize():在 Spring Security 中,@PreAuthorize 是一个用于方法级别的安全注解,它允许你在方法执行之前基于表达式来进行访问控制。当一个带有 @PreAuthorize 注解的方法被调用时,Spring Security 会先评估 @PreAuthorize 注解中的表达式。如果表达式的结果为 true,则允许方法执行;如果结果为 false,则会抛出一个 AccessDeniedException 异常,阻止方法的执行。 @PreAuthorize 注解通常包含一个字符串表达式,这个表达式可以使用 Spring Expression Language (SpEL) 来编写。表达式可以访问当前认证对象 (authentication),以及方法的参数等。常用的表达式包括但不限于: hasRole('ROLE_ADMIN'):检查用户是否具有特定的角色。 hasAuthority('DELETE_PRIVILEGE'):检查用户是否具有特定的权限。 principal.username.equals('admin'):检查当前登录用户名是否等于 'admin'。 #id > 0:检查方法参数 id 是否大于0。

这里先说使用方法:

  /*** 获取菜单结点  menu:list*/@Parameters({@Parameter(name = "Authorization", description = "请求token", required = true, in = ParameterIn.HEADER)})@Operation(summary = "获取菜单结点")@GetMapping("findNodes")@PreAuthorize("@ss.hasPerm('sys:user:select')")public Result findNodes() {List<MenuVo> menusVo = menuService.findNodes();return Result.success(menusVo);}

 这里的sys:user:select也就是在上面认证的perms

说白了就是你的角色是日志管理员,那么你的权限是系统日志这个模块,其他模块没有权限去请求。上图的perms字段是用户控制按钮的权限。  

 实现


@Component("ss")
@RequiredArgsConstructor
@Slf4j
public class PermissionService {private final RedisTemplate<String, Object> redisTemplate;private  final MenuService menuService;/*** 判断当前登录用户是否拥有操作权限** @param requiredPerm 所需权限* @return 是否有权限*/public boolean hasPerm(String requiredPerm) {if (StrUtil.isBlank(requiredPerm)) {return false;}// 超级管理员放行if (SecurityUtils.isRoot()) {return true;}// 获取当前登录用户的角色编码集合Set<String> roleCodes = SecurityUtils.getRoles();if (CollectionUtil.isEmpty(roleCodes)) {return false;}// 获取当前登录用户的所有角色的权限列表Set<String> rolePerms = this.getRolePermsFormCache(roleCodes);if (CollectionUtil.isEmpty(rolePerms)) {return false;}// 判断当前登录用户的所有角色的权限列表中是否包含所需权限boolean hasPermission = rolePerms.stream().anyMatch(rolePerm ->// 匹配权限,支持通配符(* 等)PatternMatchUtils.simpleMatch(rolePerm, requiredPerm));if (!hasPermission) {log.error("-------------------------用户无操作权限-----------------------------------");}return hasPermission;}/*** 从缓存中获取角色权限列表** @param roleCodes 角色编码集合* @return 角色权限列表*/public Set<String> getRolePermsFormCache(Set<String> roleCodes) {// 检查输入是否为空if (CollectionUtil.isEmpty(roleCodes)) {return Collections.emptySet();}Set<String> perms = menuService.listRolePerms(roleCodes);log.info("通过角色查询出来的权限列表为:{}",Arrays.toString(perms.toArray()));return perms;}
}

 建议:上面getRolePermsFormCache不应该总是去数据库查询,应该启动之前把menu加到redis中。

工具

public class SecurityUtils {/*** 获取当前登录人信息** @return SysUserDetails*/public static SysUserDetails getUser() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null) {Object principal = authentication.getPrincipal();if (principal instanceof SysUserDetails) {return (SysUserDetails) authentication.getPrincipal();}}return null;}/*** 获取用户ID** @return Long*/public static Long getUserId() {Long userId = Convert.toLong(getUser().getUserId());return userId;}/*** 获取用户角色集合** @return 角色集合*/public static Set<String> getRoles() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null) {Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();if (CollectionUtil.isNotEmpty(authorities)) {return authorities.stream().filter(item -> item.getAuthority().startsWith("ROLE_")).map(item -> StrUtil.removePrefix(item.getAuthority(), "ROLE_")).collect(Collectors.toSet());}}return Collections.EMPTY_SET;}/*** 是否超级管理员* <p>* 超级管理员忽视任何权限判断** @return*/public static boolean isRoot() {Set<String> roles = getRoles();return roles.contains("ROOT");}}

 注意:这里isRoot方面的ROOT,应该在角色的role_code 设置。

 

测试 

  @PreAuthorize("@ss.hasPerm('sys:user:ll')")public Result findNodes() {List<MenuVo> menusVo = menuService.findNodes();return Result.success(menusVo);

这里我数据库并没有sys:user:ll权限,结果为:

  @PreAuthorize("@ss.hasPerm('sys:user:select')")public Result findNodes() {List<MenuVo> menusVo = menuService.findNodes();return Result.success(menusVo);}

 

 这里没有进行前端的对接,关注后续更新对接前端哦~~~

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【数据分析预备】Numpy入门
  • 【STM32】GPIO翻转操作实现流水灯
  • C#上位机使用Microsoft.Office.Interop.Excel和EPPlus库对Excel或WPS表格进行写操作
  • 外包服务商靠谱吗?一文教你火眼金睛辨真相
  • Etherpad在线文档协作编辑工具
  • 【IOS兼容】IOS/IE new Date() 返回结果为invalid date
  • 微信支付商家转账到零钱:快速开通攻略及功能全解
  • 探索前沿科技:在本地系统上安装和使用Style TTS2进行高质量语音合成
  • word宏的概念
  • 《Python 面试热门问题五》
  • C++:类型转换
  • Linux之多线程概念
  • 深度学习5从0到1理解RNN(包括LTSM,GRU等):内容丰富(下)
  • Ubuntu22.04版本左右,开机自动启动脚本
  • 7个流行的开源数据治理工具
  • 【Under-the-hood-ReactJS-Part0】React源码解读
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • cookie和session
  • Git学习与使用心得(1)—— 初始化
  • Java-详解HashMap
  • Lucene解析 - 基本概念
  • nodejs实现webservice问题总结
  • VirtualBox 安装过程中出现 Running VMs found 错误的解决过程
  • Yeoman_Bower_Grunt
  • 关于Flux,Vuex,Redux的思考
  • 技术攻略】php设计模式(一):简介及创建型模式
  • 七牛云 DV OV EV SSL 证书上线,限时折扣低至 6.75 折!
  • 前端攻城师
  • 巧用 TypeScript (一)
  • 山寨一个 Promise
  • 设计模式(12)迭代器模式(讲解+应用)
  • 十年未变!安全,谁之责?(下)
  • 微信小程序--------语音识别(前端自己也能玩)
  • 为视图添加丝滑的水波纹
  • 协程
  • 译有关态射的一切
  • 原生JS动态加载JS、CSS文件及代码脚本
  • - 转 Ext2.0 form使用实例
  • d²y/dx²; 偏导数问题 请问f1 f2是什么意思
  • Prometheus VS InfluxDB
  • puppet连载22:define用法
  • ​sqlite3 --- SQLite 数据库 DB-API 2.0 接口模块​
  • ​如何防止网络攻击?
  • ###51单片机学习(2)-----如何通过C语言运用延时函数设计LED流水灯
  • (04)Hive的相关概念——order by 、sort by、distribute by 、cluster by
  • (13)Latex:基于ΤΕΧ的自动排版系统——写论文必备
  • (23)Linux的软硬连接
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (arch)linux 转换文件编码格式
  • (zz)子曾经曰过:先有司,赦小过,举贤才
  • (二十五)admin-boot项目之集成消息队列Rabbitmq
  • (附程序)AD采集中的10种经典软件滤波程序优缺点分析
  • (附源码)ssm基于jsp高校选课系统 毕业设计 291627
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (一)基于IDEA的JAVA基础1