SpringSecurity学习 - 认证和授权
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
我所学习的正是,认证和授权和流程。
另外默认SpringBoot、Redis会调用。
1.简单入门Demo
新建一个SpringBoot工程。这是使用的依赖。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--其他必须依赖,下面需要的--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.58</version></dependency><dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jdk8</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><!-- 导入4个jar包,解决jdk9缺失jar包引起的报错--><!-- java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter--><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><dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version></dependency>
启动之后,访问localhost:8888, 出现一下页面代表SpringSecurity生效了。
此时我们发现控制台生成:
我们向登录表单输入,这段密码和用户名user,即可登录通过。
2.认证
2.1 登录校验流程
当然,我们实际情况,可能会加一个redis做缓存,登录之后,用用户id -> 用户信息,存储到redis中。
这样我们登录之后,解析token,就是从redis查询。
2.2 SpringSecurity验证流程
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。
2.3 认证流程详解
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
2.4 思路分析
1. ...,在现在前后端分离项目,我们肯定是要自定义登录接口,不能用SpringSecurity的登录页面;
2. UserDetailService 是在内存中查找比较用户输入的用户名和密码,那我们需要是需要查询数据库比对的。
2.5 问题解决
2.5.0 前提
user.sql
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (`id` bigint NOT NULL COMMENT '主键',`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',`email` varchar(64) DEFAULT NULL COMMENT '邮箱',`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',`avatar` varchar(128) DEFAULT NULL COMMENT '头像',`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',`create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_by` bigint DEFAULT NULL COMMENT '更新人',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`del_flag` int DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
application.yaml 中数据库配置
spring:datasource:url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTCusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver
UserMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
User
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "user")
public class User {/*** 主键*/@TableIdprivate Long id;/*** 用户名*/private String userName;/*** 昵称*/private String nickName;/*** 密码*/private String password;/*** 账号状态(0正常 1停用)*/private String status;/*** 邮箱*/private String email;/*** 手机号*/private String phonenumber;/*** 用户性别(0男,1女,2未知)*/private String sex;/*** 头像*/private String avatar;/*** 用户类型(0管理员,1普通用户)*/private String userType;/*** 创建人的用户id*/private Long createBy;/*** 创建时间*/private Date createTime;/*** 更新人*/private Long updateBy;/*** 更新时间*/private Date updateTime;/*** 删除标志(0代表未删除,1代表已删除)*/private Integer delFlag;
}
UserDetailServiceImpl
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {private User user;//private List<String> authList;////@JSONField(serialize = false)//private List<SimpleGrantedAuthority> authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redis//////public UserDetailsImpl(User user, List<String> authList) { // 将对应的权限字符串列表传入// this.user = user;// this.authList = authList;//}//@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;//if(authorities != null){// return authorities;//}else{// authorities = new ArrayList<>();//}//第一次登录,封装UserDetails对象,初始化权限列表//for (String auth : authList) {// SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);// authorities.add(simpleGrantedAuthority); // 对,默认是个空的//}//return authorities;return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() { // 下面bool值,全部响应为true,UserDetail对象返回校验过程中,会因为没权限报错return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
RedisConfig : redis配置类
@Configuration
public class RedisConfig {@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}
}
Result : 后端统一封装响应
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {private T data;private String mes;private Integer code;public Result(String mes, Integer _code) {}public static <T> Result success(T data){return new Result(data,"操作成功",200);}public static <T> Result success(String _mes){return new Result(_mes,200);}public static <T> Result error(String _mes,Integer _code){return new Result(_mes,_code);}
}
JwtUtil 工具类
/*** JWT工具类*/
public class JwtUtil {//有效期为public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时//设置秘钥明文public static final String JWT_KEY = "sangeng";public static String getUUID(){String token = UUID.randomUUID().toString().replaceAll("-", "");return token;}/*** 生成jtw* @param subject token中要存放的数据(json格式)* @return*/public static String createJWT(String subject) {JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间return builder.compact();}/*** 生成jtw* @param subject token中要存放的数据(json格式)* @param ttlMillis token超时时间* @return*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if(ttlMillis==null){ttlMillis=JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setId(uuid) //唯一的ID.setSubject(subject) // 主题 可以是JSON数据.setIssuer("sg") // 签发者.setIssuedAt(now) // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate);}/*** 创建token* @param id* @param subject* @param ttlMillis* @return*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间return builder.compact();}public static void main(String[] args) throws Exception {
// String jwt = createJWT("2123");Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");String subject = claims.getSubject();System.out.println(subject);
// System.out.println(claims);}/*** 生成加密后的秘钥 secretKey* @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析** @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}}
FastJsonRedisSerializer
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes == null || bytes.length <= 0){return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}protected JavaType getJavaType(Class<?> clazz){return TypeFactory.defaultInstance().constructType(clazz);}
}
2.5.1 自定义UserDetailService
我们自定义UserDatailService 实现UserDatailService接口,注入到spring容器中,这样就会在SpringSecurity的认证流程中调用我们自定义的实现类。
@Service
public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 用用户名查询对应UserLambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUserName,username);// 判断是否空User user = this.getOne(queryWrapper);if(user == null){throw new RuntimeException("用户名或密码错误");}// 将查询到user封装到自定义UserDeatail中return new UserDetailsImpl(user);}
}
我们看下图,我们输入的用户名和密码,最终会传入UserDetailService 对象,加载loadUserByUsername 方法,返回UserDtail对象。返回过程中,会与UserDetail中的password和username进行比对,不同则报错。
注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如 : 数据库中,username:sg, password: {noop}1234
这样登陆的时候就可以用sg作为用户名,1234作为密码来登陆了。
2.5.2 密码加密存储
在实际中项目中,我一般不会明文存储,而是采用PasswordEncoder加密的方式;
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}}
2.5.3 自定义登录接口
- 接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
- 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
- 认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。
UserController
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/login")Result login(@RequestBody User user){return userService.login(user);}}
public interface UserService extends IService<User> {Result login(User user);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic Result login(User user) {// AuthenticationManager authenticationManager 进行认证UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);// 如果认证通过、给出对应提示if(Objects.isNull(authenticate)){throw new RuntimeException("登录失败");}// 认证通过使用userid生成jwtUserDetailsImpl udi = (UserDetailsImpl) authenticate.getPrincipal();String userId = udi.getUser().getId().toString();String token = JwtUtil.createJWT(userId);HashMap<String, String> map = new HashMap<>();map.put("token",token);// 把完整用户信息存入redisredisTemplate.opsForValue().set("login:" +userId,udi);return Result.success(map,"登录成功");}}
SpringSecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
spring:# mysql配置datasource:url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTCusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver# redis配置redis:# 默认0库database: 0#连接超时时间timeout: 10000msport: 6379host: 192.168.213.136lettuce:pool:# 设置最大连接数max-active: 1024# 最大阻塞时间max-wait: 10000ms# 最大空间连接,默认8max-idle: 200# 最小空间连接,默认5min-idle: 5
server:port: 8888
2.5.4 自定义jwt过滤器
以下跨域设置,postman测试是完全没有问题的。但是,我们是为了前后端分离,会有问题。
当登录之后,携带token的请求头,被jwt过滤器捕获解析之后,获得userId,用userId将从redis拿出UserDetailImpl封装到 SecurityContextHolder.getContext()中,被SpringSecurity过滤链捕获到,证明没有问题,因此放行访问资源。
/*** jwt过滤器** @author: qhx20040819* @date: 2023-09-09 21:27**/
@Component
public class JwtFilter extends OncePerRequestFilter {@Autowiredprivate RedisTemplate redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 设置跨域response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); // 修改携带cookie,PSresponse.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT");response.setHeader("Access-Control-Allow-Headers", "Authorization,content-type"); // PS// 预检请求缓存时间(秒),即在这个时间内相同的预检请求不再发送,直接使用缓存结果。response.setHeader("Access-Control-Max-Age", "3600");//获取tokenString authorization = request.getHeader("Authorization");if(StringUtils.isEmpty(authorization)){filterChain.doFilter(request,response);return;}String token = authorization.substring(6);//解析tokenString userid;try {Claims claims = JwtUtil.parseJWT(token);userid = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}//从redis中获取用户信息String redisKey = "login:" + userid;UserDetailsImpl udi = (UserDetailsImpl) redisTemplate.opsForValue().get(redisKey);if(Objects.isNull(udi)){throw new RuntimeException("用户未登录");}//存入SecurityContextHolder//获取权限信息封装到Authentication中UsernamePasswordAuthenticationToken authenticationToken = // 现在权限字段是nullnew UsernamePasswordAuthenticationToken(udi,null,udi.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}
3.授权
3.1 授权流程
在SpringSecurity中,会使用默认FiterSecuritInterceptor来进行权限校验,而它又是从SpringSecurityContex中Authentication,来获取其中的权限信息。判断当前用户是否拥有访问当前资源权限。
因此,我们只需要将权限信息存储到Authentication中即可,设置资源的访问权限。
3.2 问题解决
3.2.1 相关配置
用注解开启相关配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)
在相应的资源上开启访问权限管控。
@RequestMapping("/hello")@PreAuthorize("hasAnyAuthority('test')") // 限制访问权限字段public String hello(){return "hello";}
3.2.2 封装权限字段
我们之前UserDetailServiceImpl中loadUserByUsername()中,返回的UserDetailImpl对象,我们之前创建时,并没传入权限字段,我们先自定义权限字段模拟用户权限列表。
@Service
public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 用用户名查询对应UserLambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUserName,username);// 判断是否空User user = this.getOne(queryWrapper);if(user == null){throw new RuntimeException("用户名或密码错误");}List<String> authlist = new ArrayList<>(); // 暂且封装这样权限字段authlist.add("test");authlist.add("test0");return new UserDetailsImpl(user,authlist);}
}
相应的UserDetailImpl中也需要修改,seucrity过滤器链是从getAuthorities()方法中来获取用户权限字段的。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {private User user;private List<String> authList;@JSONField(serialize = false)private List<SimpleGrantedAuthority> authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redispublic UserDetailsImpl(User user, List<String> authList) { // 将对应的权限字符串列表传入this.user = user;this.authList = authList;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {// 初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;if(authorities != null){return authorities;}else{authorities = new ArrayList<>();}// 第一次登录,封装UserDetails对象,初始化权限列表for (String auth : authList) {SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);authorities.add(simpleGrantedAuthority); // 对,默认是个空的}return authorities;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
这是拿着携带token去请求,能访问成功。
3.2.3 数据库查询权限字段
就是我们真实的权限应该封装在数据库中。
3.2.3.1 RBAC权限模型
一个用户,可以对应多个角色;一个角色,可以对应多个用户;
一个角色,可以拥有多个权限字段; 一个权限字段,可以被多个角色所拥有;
3.2.3.2 准备工作
USE `test`;/*Table structure for table `menu` */DROP TABLE IF EXISTS `menu`;CREATE TABLE `menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',`path` varchar(200) DEFAULT NULL COMMENT '路由地址',`component` varchar(255) DEFAULT NULL COMMENT '组件路径',`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',`create_by` bigint(20) DEFAULT NULL,`create_time` datetime DEFAULT NULL,`update_by` bigint(20) DEFAULT NULL,`update_time` datetime DEFAULT NULL,`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';/*Table structure for table `role` */DROP TABLE IF EXISTS `role`;CREATE TABLE `role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(128) DEFAULT NULL,`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',`create_by` bigint(200) DEFAULT NULL,`create_time` datetime DEFAULT NULL,`update_by` bigint(200) DEFAULT NULL,`update_time` datetime DEFAULT NULL,`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';/*Table structure for table `role_menu` */DROP TABLE IF EXISTS `role_menu`;CREATE TABLE `role_menu` (`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;/*Table structure for table `user` */DROP TABLE IF EXISTS `user`;CREATE TABLE `user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',`email` varchar(64) DEFAULT NULL COMMENT '邮箱',`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',`avatar` varchar(128) DEFAULT NULL COMMENT '头像',`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';/*Table structure for table `user_role` */DROP TABLE IF EXISTS `user_role`;CREATE TABLE `user_role` (`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SELECT DISTINCT m.`perms`
FROMuser_role urLEFT JOIN `role` r ON ur.`role_id` = r.`id`LEFT JOIN `role_menu` rm ON ur.`role_id` = rm.`role_id`LEFT JOIN `menu` m ON m.`id` = rm.`menu_id`
WHEREuser_id = 2AND r.`status` = 0AND m.`status` = 0
@TableName(value="menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {private static final long serialVersionUID = -54979041104113736L;@TableIdprivate Long id;/*** 菜单名*/private String menuName;/*** 路由地址*/private String path;/*** 组件路径*/private String component;/*** 菜单状态(0显示 1隐藏)*/private String visible;/*** 菜单状态(0正常 1停用)*/private String status;/*** 权限标识*/private String perms;/*** 菜单图标*/private String icon;private Long createBy;private Date createTime;private Long updateBy;private Date updateTime;/*** 是否删除(0未删除 1已删除)*/private Integer delFlag;/*** 备注*/private String remark;
}
3.2.3.3 代码实现
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {List<String> selectPermsByUserId(Long userId);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qhx.springsecuritydemo.mapper.MenuMapper"><select id="selectPermsByUserId" resultType="java.lang.String">SELECTm.permsFROM`user_role` as urLEFT JOIN `role` as r on ur.role_id = r.idLEFT JOIN `role_menu` as rm on ur.role_id = rm.role_idLEFT JOIN `menu` as m on rm.menu_id = m.idWHEREur.`user_id` = #{userId} AND r.`status`=0 and m.`status`=0;</select></mapper>
修改UserDetailServiceImpl。
@Service
public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {@Autowiredprivate MenuMapper menuMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 用用户名查询对应UserLambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUserName,username);// 判断是否空User user = this.getOne(queryWrapper);if(user == null){throw new RuntimeException("用户名或密码错误");}List<String> authList = menuMapper.selectPermsByUserId(user.getId());return new UserDetailsImpl(user,authlist);}
}
4.异常处理
4.1 SpringSecurity异常处理
我们发现,就算我们登录输入的用户名和错误的:
1.SpringSecurity没有抛出异常 2.也有没有相关响应提示信息;
我是并不希望这样,因此要添加自定义异常处理器:
/*** 授权异常处理器**/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {Result result = Result.error("权限不足", 403);String jsonResult = JSON.toJSONString(result);response.setStatus(200);response.setContentType("application/json");response.getWriter().write(jsonResult);}
}
/*** 认证异常处理器**/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {Result result = Result.error("您认证错误/请检查你的用户名或密码是否正确", 401);String jsonResult = JSON.toJSONString(result);response.setStatus(200);response.setContentType("application/json");response.getWriter().write(jsonResult);}
}
在SecurityConfig配置中引入
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtFilter jwtFilter;@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;@Autowiredprivate AuthenticationEntryPoint authenticationEntryPoint;@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 添加自定义token过滤器到链中http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);// 添加自定义异常处理器http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint);}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
4.2 自定义异常处理
上面除了我们SpringSecurity异常,那么还有一些其他异常,我们不希望每次捕获,并手动抛出,因此可以转化为自定义异常,用SpringBoot的全局异常处理器捕获并且抛出;
5. 跨域
我们现在是用postman测试,但是未来项目是设计到前后端分离项目交互的,都伴随着跨域的问题。
后端能接受到前端原生request对象,response对象,都涉及到跨域的问题。
SpringSecurity解决跨域
SringMvc解决跨域
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}
}
自定义jwt过滤器解决跨域
相关文章:

SpringSecurity学习 - 认证和授权
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。 一般Web应用的需要进行认证和授权。 认证:验证当前访问系统的是不是本系统的用户,并且要…...

JDK jps命令复习
之前写过jdk命令工具的博文,下面复习jps命令; jps 是 Java Process Status Tool 的简称,它的作用是为了列出所有正在运行中的 Java 虚拟机进程和相关信息; jps 命令参数 -q 只输出进程 ID,省略主类的名称 -m 输出虚拟机进程启动时传递…...

Android 13.0 屏蔽Launcher3桌面app图标的长按功能
1.概述 在13.0的产品定制化开发中,系统默认的Launcher3在workspace 第二屏通常都会显示app列表 点击进入app 列表页,长按app的icon图标会弹出 应用信息 弹窗 等信息,而产品的开发需要,不需要弹出这些信息,所以要求去掉app的icon图标的长按功能 2.屏蔽Launcher3桌面app图…...

软考和PMP哪个含金量更高?
软考中,能和pmp一起来比较的是软考高项,软考高级信息系统项目管理师,和PMP的共同点,基本来说都是项目管理类的证书。本质也都是适用于项目经理岗位的证书,软考高项中大部分考试内容是PMPIT技术两部分,其中项…...

第一章:最新版零基础学习 PYTHON 教程(第三节 - 下载并安装Python最新版本)
在这里,我们将讨论如何获得与在 Windows/Linux/mac OS 上安装 Python 相关的所有问题的答案。Python由Guido van Rossum于20世纪90年代初开发,最新版本为3.11,我们可以简称为Python3。 如何下载并安装Python? 要了解如何安装 P…...

Spring 中三种 BeanName 生成器!
无论我们是通过 XML 文件,还是 Java 代码,亦或是包扫描的方式去注册 Bean,都可以不设置 BeanName,而 Spring 均会为之提供默认的 beanName,今天我们就来看看 Spring 中三种处理不同情况的 beanName 生成器。 1. BeanN…...

Go基础-文件、字符
文件创建 导入“os”包,创建文件,读写文件的函数都在改包。 指定创建的文件存放路径以及文件名。 执行Create( )函数,进行文件创建。 关闭文件。 package mainimport ("fmt""os" )func main() {//创建文件,…...

启动YOLO进行图片物体识别
查看官方文档YOLO: Real-Time Object Detection 这些是一些模型的对比,显示了YOLO的优势,继续往下面看 CoCoData set 是一个数据库,用来训练模型,这里面有丰富的物体检测,分割数据集,图像经过了精确的segm…...

BMS电池管理系统的蓝牙芯片 国产高性能 低功耗蓝牙Soc芯片PHY6222
电池管理系统是对电池进行监控与控制的系统,将采集的电池信息实时反馈给用户,同时根据采集的信息调节参数,充分发挥电池的性能。但是,前技术中,在管理多个电池时,需要人员现场调试与设置,导致其…...

肖sir__mysql之三表__008
mysql之三表 create table student( stu_no int, stu_name varchar(10), sex char(1), age int(3), edit varchar(20) ) DEFAULT charsetutf8; insert into student values (1,‘wang’,‘男’,21,‘hello’), (2,‘小明’,‘女’,22,‘haha2’), (3,‘hu’,‘女’,23,‘haha3…...

【Linux】常用工具(上)
Linux 常用工具 一、Linux 软件包管理器 yum1. 软件包2. 查看软件包3. 安装/卸载软件4. yum 其他指令的功能 二、Linux 编辑器 - vim 使用1. vim 的基本概念2. vim 的基本操作(1)光标移动(命令模式)(2)光标…...

【kafka】可视化工具KAFKA EAGLE安装分享
目录 准备: 开始: 1.解压 2.环境变量配置 3.生效环境变量配置文件 3.修改配置文件 1.修改zookeeper集群信息 2.修改mysql配置信息 4.启动 5.异常排查 6.页面 创作不易,你的动力是我创作的动力,如果有帮助请关注我&…...

【深度装机】深度U盘装机后黑屏闪光标
装x64的系统 装机版的gho文件太多预装软件了。几年前买的这种无风扇的机器,之前装的ubuntu:装机U盘启动后,先删掉的所有的ubuntu分区。使用了10几年的AOC又肩负使命: 感觉发热还是挺大的。 有人说因为secure boot 打开secure boo…...

【性能测试】JMeter:集合点,同步定时器的应用实例!
一、集合点的定义 在性能测试过程中,为了真实模拟多个用户同时进行操作以度量服务器的处理能力,可以考虑同步虚拟用户以便恰好在同一时刻执行操作或发送请求。 通过插入集合点可以较真实模拟多个用户并发操作。 (注意:虽然通过加入集合点可…...

21天学会C++:Day11----运算符重载
CSDN的uu们,大家好。这里是C入门的第十一讲。 座右铭:前路坎坷,披荆斩棘,扶摇直上。 博客主页: 姬如祎 收录专栏:C专题 目录 1. 知识引入 2. 运算符重载 2.1 operator<() 2.2 operator() 2.3 o…...

面经pc端项目
创建项目 安装脚手架-----创建项目------选择自定义 sass基础语法 https://www.sass.hk/ sass语法有两个:sass(旧) scss(新) 1.scss语法 和less语法类似,支持嵌套,支持变量… scss: $变量名 less: @变量名 $color:orange; .box{width: 400px;height: 400px;borde…...

三步在两台服务器间迁移conda环境
引言: 背景是我现在要跑的实验在一台服务器上跑有点来不及了,需要将conda环境和文件一起迁移到另一台服务器上。文件的迁移可以用scp或者rsync。但是conda虚拟环境的迁移则不行。 步骤: step 1 将当前的虚拟幻境信息写入environment.yml c…...

websocket服务部署在内网,app无法访问
描述:websocket服务部署在内网(ws://),app无法访问外网,需要将内网地址映射到外网 解法:通过nginx配置ws代理,部署nginx的服务器连通的外网和内网,通过nginx配置将原有的ws请求转换…...

vs2010对于c++11的支持
不支持 using代替typedef变参模板for each...

OpenCV cv::Mat和QImage互相转换
在使用OpenCV和Qt进行图像处理时,经常需要将cv::Mat和QImage之间进行转换。下面是cv::Mat和QImage之间的相互转换方法: 将cv::Mat转换为QImage: cv::Mat cvImage; // 你的cv::Mat图像// 将cv::Mat转换为QImage QImage qtImage(cvImage.data…...

pgsql 主从搭建
在 PostgreSQL 中,主从复制(Master-Slave Replication)是一种常见的数据库高可用性和数据备份解决方案。它允许你创建一个主数据库服务器(Master),并在一个或多个从数据库服务器(Slaveÿ…...

JS中的数值精度问题(二)
一、精度范围,精度最多只能到53个二进制位 JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。 国际标准IEEE 754规定,有效数字第一位默认总是…...

WPF——Control与Template理解
文章目录 一、前言二、控件三、模板3.1 DataTemplate3.2 ControlTemplate3.3 ContentPresenter 四、结语 一、前言 最近又翻看了下刘铁猛的《深入浅出WPF》,发现对模板章节中的部分内容有了更深的体会,所以写篇文扯扯。 文章标题是Control与Template&a…...

华为HCIA学习(一)
文章目录 一.根据考试题总结知识点(一题一点)二.上午学习三.下午学习四.今天只做了70题,需要的可以找我 一.根据考试题总结知识点(一题一点) 二.上午学习 ① VRP系统是VRP是华为公司从低端到高端的全系列路由器、交换…...

使用jmeter+ant+jenkins+git搭建自动化测试平台
最近正在学习自动化测试,于是随手搭建了一下jmeterantjenkinsgit平台。 接下来,我会按照jdk,jmeter,ant,jenkins,git这个顺序一步一步的搭建起来。 一、jdk。这个就不多说了。我用的是1.8版本的,配环境变…...

C# Winform中在DataGridView中添加Button按钮,操作Button按钮
.Net的DataGridView控件中,提供了一种列的类型,叫 DataGridViewButtonColumn ,这种列类型是展示为一个 按钮,可以给button赋予相应的text,并且,此button可以用来做处理事件的判断依据。 DataGridViewButto…...

Docker 网络学习
docker的网络模式 当你开始大规模使用Docker时,你会发现需要了解很多关于网络的知识。Docker作为目前最火的轻量级容器技术,有很多令人称道的功能,如Docker的镜像管理。然而,Docker同样有着很多不完善的地方,网络方面…...

django创建web服务器
安装 pip install django 创建项目 django-admin startproject report django-admin startapp data //project下可创建多个app 执行使用 python manage.py migrate //orm代码到数据库 python manage.py runserver 0.0.0.0:80 权限管理 python manage.py createsuperuser 创建…...

极光笔记 | 推送服务数据中心选择:合规性与传输效率的双重考量
随着全球化进程的深入,跨境数据传输与存储问题已经变得愈发重要。推送服务的数据中心节点选择不仅关乎数据访问速度和用户体验,同时也直接牵扯到数据合规性和安全保障。EngageLab Push深知这一点,为了满足更多国际客户和全球用户触达需求&…...

Python灰帽编程——初识Python上
1. Python 简介 常用安全工具语言示例perljoomscan whatwebrubymetasploit-frameworkpythonsqlmap pocsuite3gogoby 1.1 Python 起源 1.1.1 语言的作者 贵铎范罗萨姆(Guido van Rossum)荷兰人于1989 年圣诞节始创了python。 大神就是大神࿰…...