Spring Security
简介#
Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
2. 认证#
2.1 登陆校验流程#
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。
流程:#
请求会经过一下三个核心过滤器。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。
认证流程#
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
整体处理思路#
登录
①自定义登录接口
controller写一个接口,service层中,对用户进行认证
先使用 UsernamePasswordAuthenticationToken 将用户名密码传入
使用authenticationManager.authenticate进行认证。
通过则生成一个jwt令牌。
把完整的用户信息存入redis userid作为key
②自定义UserDetailsService
创建一个UserDetailsService 实现类,在这里进行数据查询,根据用户名查询数据。
创建一个loginUser 实现UserDetails,重写全部方法,将所有的false改成true,
将数据封装成userDetails返回。
校验:
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
快速入门#
准备工具类,坐标依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--fastjson依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.33</version> </dependency> <!--jwt依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
redis工具类#
/** * Redis使用FastJson序列化 * * @author sg */ 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; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public 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); } }
@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; } }
/** * 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 token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg"; Claims claims = parseJWT(token); 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(); } }
@SuppressWarnings(value = { "unchecked", "rawtypes" }) @Component public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 获得缓存的基本对象。 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection 多个对象 * @return */ public long deleteObject(final Collection collection) { return redisTemplate.delete(collection); } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * * @param key * @return */ public <T> Set<T> getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 缓存Map * * @param key * @param dataMap */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的Map * * @param key * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 获取Hash中的数据 * * @param key Redis键 * @param hKey Hash键 * @return Hash中的对象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 删除Hash中的数据 * * @param key * @param hkey */ public void delCacheMapValue(final String key, final String hkey) { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.delete(key, hkey); } /** * 获取多个Hash中的数据 * * @param key Redis键 * @param hKeys Hash键集合 * @return Hash对象集合 */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); }
public class WebUtils { /** * 将字符串渲染到客户端 * * @param response 渲染对象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }
接下来进行一些常规操作,创建数据库,创建实体类,result类,配置数据库源,创建三层架构。
核心代码实现#
实现userDatils 接口#
创建一个类,实现userDatils,这样就自定义了userDatils,重新里面的方法,之后数据库查询到的密码需要封装成UserDetails,还需要在里面创建一个user成员变量。
@Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
实现UserDetailsService接口#
重写其中的方法。更改为 用户名从数据库中查询用户信息。
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根据用户名查询用户信息 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUserName,username); User user = userMapper.selectOne(wrapper); //如果查询不到数据就通过抛出异常来给出提示 if(Objects.isNull(user)){ throw new RuntimeException("用户名或密码错误"); } //根据用户查询权限信息 添加到LoginUser中 //封装成UserDetails对象返回,就是上面创建的。 return new LoginUser(user); } }
更改密码加密方式#
此时还要注意一个问题,就是密码存储在数据库中的加密,默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 但是不建议使用,此处我们采用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
如下:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
创建登录接口#
@RestController public class LoginController { @Autowired private LoginServcie loginServcie; @PostMapping("/user/login") public ResponseResult login(@RequestBody User user){ //登录 return loginServcie.login(user); }
创建一个拦截器#
这里问你只需要在刚才定义加密方法的配置类中再重写一个configure(HttpSecurity http)方法,将下面的配置都写上即可。
注入一个bean即可,点击重写方法选择authenticationManagerBean(),在上面加上@Bean注解。
用于用户认证,注入到实现层使用。
@Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); } //注入authenticationManagerBean 用于用户认证,注入到实现层使用。 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
业务实现层(login)#
LoginServiceImpl
- 首先这里我们要进行用户名和密码验证,以及身份认证。
- 注入刚刚配置类中的authenticationManager进行身份认证。
- 身份认证需要用到,authenticate,这是一个接口要使用的话需要实现类,通过ctrl+左键,进入源码: Authentication authenticate(Authentication var1)throwsAuthenticationException;
- 按住ctrl+alt+左键 用来查看实现方法。
- 此处我们选择UsernamePasswordAuthenticationToken即可。将用户的用户名和密码传入。认证成功则拿到userId生成jwt令牌。
获取userid方法#
通过断电调试的方法知道userid在authenticate的Principal,类型是loginuser,拿到之后强转,通过loginuser获取id。
jwt令牌的知识简单补充。#
-
JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
-
第一部分:Header(头), 记录令牌类型、签名算法等。 例如:
-
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:
-
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
-
签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。
只需要导入依赖和工具类,前面已经做过。
@Service public class LoginServiceImpl implements LoginServcie { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult login(User user) { //AuthenticationManager authenticate进行用户认证 UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(userToken); //如果认证没通过,给出对应的提示 if (Objects.isNull(authenticate)){ throw new RuntimeException("登录失败"); } //如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = String.valueOf(loginUser.getUser().getId()); String jwt = JwtUtil.createJWT(userId); Map<String,String> map =new HashMap(); map.put("token",jwt); //把完整的用户信息存入redis userid作为key redisCache.setCacheObject("login",userId,loginUser); return new ResponseResult<>(200,"登陆成功",map); } }
设置jwt过滤器#
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。
使用userid去redis中获取对应的LoginUser对象。
然后封装Authentication对象存入SecurityContextHolder
-
获取token 解析token
从请求头中获取携带的token进行判断,空则直接放行。
解析通过getSubject获取id。 -
从redis中获取用户信息
注入rediscache,获取秘钥。 -
存入SecurityContextHolder,获取权限信息封装到Authentication中
使用三个参数的认证,代表是已认证过得放行,最后将权限信息封装进Authentication。 -
放行
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //获取token String token = request.getHeader("token"); if (StringUtils.isEmpty(token)) { filterChain.doFilter(request, response); return; } //解析token String userId; try { Claims claims = JwtUtil.parseJWT(token); userId = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //从redis中获取用户信息 String redisKey = "login" + userId; LoginUser loginUser= redisCache.getCacheObject(redisKey); //存入SecurityContextHolder // 获取权限信息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null); //必须传递三个参数的。 SecurityContextHolder.getContext().setAuthentication(authenticationToken); //放行 filterChain.doFilter(request, response); } }
调整过滤器的顺序#
当我们写完jwt的过滤器之后要调整顺序,否则会被FilterSecurityInterceptor拦截,就无法生效了。
只需要在之前的配置类中加上一个http.addFilterBefore,在配置类中注入刚刚书写的jwt过滤器
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; //添加过滤器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
退出登录#
同一次请求
@Override public ResponseResult logout() { //获取SecurityContextHolder中的用户id UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userid = loginUser.getUser().getId(); //删除redis中的值 redisCache.deleteObject("login:"+userid); return new ResponseResult(200,"注销成功"); }
授权功能
不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。
授权基本流程#
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限即可。
限制访问资源所需权限#
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
但是要使用它我们需要先开启相关配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)
然后就可以使用对应的注解。@PreAuthorize
例子:
@RestController
public class HelloController {
@RequestMapping("/hello") @PreAuthorize("@ex.hasAuthority('system:dept:list')") // @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')") // @PreAuthorize("hasRole('system:dept:list')") // @PreAuthorize("hasAnyRole('admin','system:dept:list')") public String hello(){ return "hello"; }
封装权限信息#
我们前面在写UserDetailsServiceImpl的时候说过,在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。
我们先直接把权限信息写死封装到UserDetails中进行测试。
我们之前定义了UserDetails的实现类LoginUser,想要让其能封装权限信息就要对其进行修改。
@Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; //存储权限信息 private List<String> permissions; public LoginUser(User user,List<String> permissions) { this.user = user; this.permissions = permissions; } //存储SpringSecurity所需要的权限信息的集合 @JSONField(serialize = false) private List<GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if(authorities!=null){ return authorities; } //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中 authorities = permissions.stream(). map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
LoginUser修改完后我们就可以在UserDetailsServiceImpl中去把权限信息封装到LoginUser中了。我们写死权限进行测试,后面我们再从数据库中查询权限信息。
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUserName,username); User user = userMapper.selectOne(wrapper); if(Objects.isNull(user)){ throw new RuntimeException("用户名或密码错误"); } //TODO 根据用户查询权限信息 添加到LoginUser中 List<String> list = new ArrayList<>(Arrays.asList("test")); return new LoginUser(user,list); } }
从数据库查询权限信息#
RBAC权限模型
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
可以这样理解,对于一对多的关系可以创建一个中间表进行关联,一个管理员可以有多少功能就有多少行。1-1 1-2 说明着拥有的功能。
数据库准备#
CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */; USE `sg_security`; /*Table structure for table `sys_menu` */ DROP TABLE IF EXISTS `sys_menu`; CREATE TABLE `sys_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 `sys_role` */ DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_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 `sys_role_menu` */ DROP TABLE IF EXISTS `sys_role_menu`; CREATE TABLE `sys_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 `sys_user` */ DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_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 `sys_user_role` */ DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_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;
自定义失败处理#
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
①自定义实现类
授权异常
@Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足"); String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }
认证异常
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录"); String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }
配置spring security
@Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; //添加过滤器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置异常处理器 http.exceptionHandling() //配置认证失败处理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允许跨域 http.cors(); }
跨域问题#
设置cors
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override public void addCorsMappings(CorsRegistry registry) { // 设置允许跨域的路径 registry.addMapping("/**") // 设置允许跨域请求的域名 .allowedOriginPatterns("*") // 是否允许cookie .allowCredentials(true) // 设置允许的请求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 设置允许的header属性 .allowedHeaders("*") // 跨域允许时间 .maxAge(3600); }
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通