SpringSecurity
SpringSecurity简介
ref:
Spring Security,这是一种基于 Spring AOP
和 Servlet
过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。本教程对 Spring Security 的使用进行一个比较全面的简要介绍。(摘自w3cschool)
创建工程与引入依赖
创建springboot项目或空maven项目
parent
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent>
dependencys
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </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-security</artifactId> </dependency>
@RestController @RequestMapping("/hello") public class HelloController { @GetMapping public String hello(){ return "hello , spring security!"; } }
启动项目访问/hello,会跳转到/login,这是默认的
流程
登录校验流程
- UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
- ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
- FilterSecurityInterceptor:负责权限校验的过滤器。
登录拦截思路
登录
①自定义登录接口 调用ProviderManager的方法进行认证如果认证通过生成jwt 把用户信息存入redis中 ②自定义UserDetailsService 在这个实现类中去查询数据库
校验:
①定义Jwt认证过滤器 获取token 解析token获取其中的userid 从redis中获取用户信息 存入SecurityContextHolder
引入redis
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
工具类与配置类
@Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisCacheAutoConfiguration { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用fastJson template.setValueSerializer(fastJsonRedisSerializer); // hash的value序列化方式采用fastJson template.setHashValueSerializer(fastJsonRedisSerializer); template.afterPropertiesSet(); return template; } }
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } private final Class<T> clazz; public FastJsonRedisSerializer(Class<T> clazz) { super(); this.clazz = clazz; } /** * 序列化 */ @Override public byte[] serialize(T t) throws SerializationException { if (null == t) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } /** * 反序列化 */ @Override public T deserialize(byte[] bytes) throws SerializationException { if (null == bytes || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return (T) JSON.parseObject(str, clazz); } }
public class JwtUtil { // 默认 生成之后有效时长 1小时 private static final Long EXPIRE= 60 * 60 * 1000L; // 密钥明文 private static final String JWT_KEY= "securityKey";// 不能有_ public static String getUUID(){ return UUID.randomUUID().toString().replaceAll("-", ""); } /** * 生成jwt * @param subject token存放的数据 json格式 * @return String */ public static String createJwt(String subject){ JwtBuilder builder = getJwtBuilder(subject, null, getUUID()); return builder.compact(); } /** * 生成jwt * @param subject token存放的数据 json格式 * @param expire 有效时间 * @return String */ public static String createJwt(String subject, Long expire){ JwtBuilder builder = getJwtBuilder(subject, expire, getUUID()); return builder.compact(); } /** * 生成jwt * @param subject token存放的数据 json格式 * @param expire 有效时间 * @param id id * @return String */ public static String createJwt(String subject, Long expire, String id){ JwtBuilder builder = getJwtBuilder(subject, expire, id); return builder.compact(); } /** * * @param subject subject * @param expire 过期时长 null 时 默认使用EXPIRATION,1小时 * @param uuid uuid * @return JwtBuilder */ private static JwtBuilder getJwtBuilder(String subject, Long expire, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if (expire == null){ expire = JwtUtil.EXPIRE; } long expMillis = nowMillis + expire; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) // 唯一id .setSubject(subject) // 主题,可以是json .setIssuer("ctp") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) // 使用HS256算法签名,第二个时密钥 .setExpiration(expDate); // 过期时间 } /** * 生成加密密钥 * @return SecretKey */ public static SecretKey generalKey(){ byte[] decodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKeySpec key = new SecretKeySpec(decodeKey, 0, decodeKey.length, "AES"); return key; } /** * 解析jwt * @param jwt jwt * @return Claims */ public static Claims parseJwt(String jwt){ SecretKey secretKey = generalKey(); Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); return claims; } }
@Component public class RedisUtil { @Resource private RedisTemplate<Object, Object> redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @return 缓存的对象 */ public ValueOperations<Object, Object> setCacheObject(Object key, Object value) { ValueOperations<Object, Object> operation = redisTemplate.opsForValue(); operation.set(key, value); return operation; } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 * @return 缓存的对象 */ public ValueOperations<Object, Object> setCacheObject(Object key, Object value, Integer timeout, TimeUnit timeUnit) { ValueOperations<Object, Object> operation = redisTemplate.opsForValue(); operation.set(key, value, timeout, timeUnit); return operation; } /** * 获得缓存的基本对象。 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public Object getCacheObject(Object key) { ValueOperations<Object, Object> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key key */ public void deleteObject(Object key) { redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection collection */ public void deleteObject(Collection<Object> collection) { redisTemplate.delete(collection); } public Long getExpire(String key) { return redisTemplate.getExpire(key); } public void expire(String key, int expire, TimeUnit timeUnit) { redisTemplate.expire(key, expire, timeUnit); } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public ListOperations<Object, Object> setCacheList(Object key, List<Object> dataList) { ListOperations<Object, Object> listOperations = redisTemplate.opsForList(); if (null != dataList) { int size = dataList.size(); for (Object o : dataList) { listOperations.leftPush(key, o); } } return listOperations; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public List<Object> getCacheList(String key) { List<Object> dataList = new ArrayList<>(); ListOperations<Object, Object> listOperation = redisTemplate.opsForList(); Long size = listOperation.size(key); if (null != size) { for (int i = 0; i < size; i++) { dataList.add(listOperation.index(key, i)); } } return dataList; } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public BoundSetOperations<Object, Object> setCacheSet(String key, Set<Object> dataSet) { BoundSetOperations<Object, Object> setOperation = redisTemplate.boundSetOps(key); for (Object o : dataSet) { setOperation.add(o); } return setOperation; } /** * 获得缓存的set * * @param key key * @return return */ public Set<Object> getCacheSet(Object key) { Set<Object> dataSet = new HashSet<>(); BoundSetOperations<Object, Object> operation = redisTemplate.boundSetOps(key); dataSet = operation.members(); return dataSet; } /** * 缓存Map * * @param key key * @param dataMap dataMap * @return return */ public HashOperations<Object, Object, Object> setCacheMap(Object key, Map<Object, Object> dataMap) { HashOperations<Object, Object, Object> hashOperations = redisTemplate.opsForHash(); if (null != dataMap) { for (Map.Entry<Object, Object> entry : dataMap.entrySet()) { hashOperations.put(key, entry.getKey(), entry.getValue()); } } return hashOperations; } /** * 获得缓存的Map * * @param key key * @return return */ public Map<Object, Object> getCacheMap(Object key) { return redisTemplate.opsForHash().entries(key); } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<Object> keys(String pattern) { return redisTemplate.keys(pattern); } }
public class WebUtil { /** * 将字符串渲染到客户端 * @param resp resp * @param str str * @return return */ public static String renderString(HttpServletResponse resp, String str){ try{ resp.setStatus(200); resp.setContentType("application/json"); resp.setCharacterEncoding("utf-8"); resp.getWriter().write(str); }catch (Exception e){ e.printStackTrace(); } return null; } }
@Data @AllArgsConstructor @NoArgsConstructor public class Ret<T> { private Integer code; private String msg; private T data; public static Ret ok(){ return new Ret(200,"ok",null); } public static Ret fail(){ return new Ret(200,"ok",null); } public Ret data(T data){ this.data = data; return this; } }
引入mybatis-plus
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency>
application.yml
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: "0101" url: jdbc:mysql://localhost:3306/db_learn?charsetEncoding=utf8&serverTimeZone=UTC
数据库表与实体类
CREATE TABLE `sec_user` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `role` tinyint DEFAULT NULL, `status` tinyint DEFAULT NULL, PRIMARY KEY (`id`) )
@Data @AllArgsConstructor @NoArgsConstructor @TableName("sec_user") public class User implements Serializable { private static final long serialVersionUID = -4445259547431031783L; @TableId private Long id; private String name; private String password; private Integer role; private Integer status; }
测试mybatisplus
@Test public void testMybatisPlus(){ List<User> users = userMapper.selectList(null); System.out.println(users); }
校验用户
@Service public class UserDetailServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户信息 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getName, username); User user = userMapper.selectOne(queryWrapper); if (Objects.isNull(user)){ throw new RuntimeException("用户名或者密码错误"); } // TODO 查询用户权限信息 // 把数据封装成UserDetail并返回 return new LoginUser(user); } }
@Data @AllArgsConstructor @NoArgsConstructor public class LoginUser implements UserDetails { private static final long serialVersionUID = -3470371303923656730L; private User user; /** * * @return 权限信息 */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getName(); } /** * * @return true -> 登录状态未过期 */ @Override public boolean isAccountNonExpired() { return true; } /** * * @return true -> 帐户未锁定 */ @Override public boolean isAccountNonLocked() { return true; } /** * * @return true -> 签发的凭据未过期 */ @Override public boolean isCredentialsNonExpired() { return true; } /** * * @return true -> 该用户是正常的 */ @Override public boolean isEnabled() { return true; } }
注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如
密码问题PasswordEncoder
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为: {id}password。 它会根据id去判断密码的加密方式。但是我们一般不会采这种方式。所以就需要替换
PasswordEncoder.
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就 会使用该PasswordEncoder来进行密码校验。
我们可以定义-个SpringSecurity的配置类,SpringSecurity要求这 个配置类要继承WebSecurityConfigurerAdapter。
关于PasswordEncoder,每次加密的盐值不一样,相同的密码最后生成的密文不一样
加密
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encode1 = passwordEncoder.encode("123456"); String encode2 = passwordEncoder.encode("123456"); System.out.println(encode1); System.out.println(encode2);
输出
$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK $2a$10$4.XsTRDY.U0AEPESe8ZZxu4VoCv3UnrrAejnQp.xC6EcDO6jmCQEu
比对
boolean b1 = passwordEncoder .matches("123456", "$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK"); boolean b2 = passwordEncoder .matches("1234", "$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK"); System.out.println(b1);// true System.out.println(b2);// false
安装Redis for windows
安装 redis for windows
https://blog.csdn.net/qq_52385631/article/details/122771598
使用教程
https://blog.csdn.net/weixin_61594803/article/details/122695446
大概步骤
1.下载 redis for windows
2.安装,安装,一路next
(密码设置 安装目录下 redis.windows-service.conf 找到 requirepass ,并设置密码,格式 requirepass xxx)
3.启动redis服务,此电脑->管理->服务
4.安装目录下启动redis.cli,界面显示主机名端口
登录接口
自定义登登录接口,然后让SpringSecurity对这 个接口放行,让用户访问这个接口的时候不用登录也能访问。在接口中我们通过AuthenticationManager的
authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话要生成一个jwt, 放入响应中返回。并粗为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为
key
SecurityConfig重写两个方法
@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(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@RestController public class LoginController { @Resource private LoginService loginService; @PostMapping("/user/login") public Ret login(@RequestBody User user){ return loginService.login(user); } }
@Service public class LoginServiceImpl implements LoginService { @Resource private AuthenticationManager authenticationManager; @Resource private RedisUtil redisUtil; @Override public Ret login(User user) { // AuthenticationManager authenticate进行用户认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword()); Authentication authentication = authenticationManager.authenticate(authenticationToken);// 调用UserDetailServiceImpl.loadUserByUsername认证 // 未通过则给相应的提示 if (Objects.isNull(authentication)) { throw new RuntimeException("登录失败"); } // 认证通过,使用用户的id生成一个jwt LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long id = loginUser.getUser().getId(); String jwt = JwtUtil.createJwt(id.toString()); // 完整信息存入redis,用户的id作为key redisUtil.setCacheObject("login:" + id, loginUser); Map<String, String> map = new HashMap<>(); map.put("token", jwt); return Ret.ok().data(map); } }
访问 http://localhost:8080/user/login
前提需要redis服务,否则失败
{ "code": 200, "msg": "ok", "data": { "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxOTRhMzlmMTdlOGI0OTQyODcyZjAyODc2OGJlMDBmYiIsInN1YiI6IjEiLCJpc3MiOiJjdHAiLCJpYXQiOjE2NzA1OTUxOTIsImV4cCI6MTY3MDU5ODc5Mn0.5-m6NmJJRy4DpSbnxY1T-BFEE7vi4P0RB7-kVUD80Ns" } }
检验token
Claims claims = JwtUtil.parseJwt("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxOTRhMzlmMTdlOGI0OTQyODcyZjAyODc2OGJlMDBmYiIsInN1YiI6IjEiLCJpc3MiOiJjdHAiLCJpYXQiOjE2NzA1OTUxOTIsImV4cCI6MTY3MDU5ODc5Mn0.5-m6NmJJRy4DpSbnxY1T-BFEE7vi4P0RB7-kVUD80Ns"); String subject = claims.getSubject(); System.out.println(subject);// 1 (iuserId)
上面代码可用获取到用户id
token认证过滤器
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Resource private RedisUtil redisUtil; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { // 获取token String token = httpServletRequest.getHeader("token"); if (StringUtils.isEmpty(token)){ filterChain.doFilter(httpServletRequest, httpServletResponse); 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 = (LoginUser)redisUtil.getCacheObject(redisKey); if (Objects.isNull(loginUser)){ throw new RuntimeException("用户未登录"); } // 存入SecurityHolder // TODO 获取权限信息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);// authorities表示授权信息 SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 放行 filterChain.doFilter(httpServletRequest, httpServletResponse); } }
添加到配置SecurityConfig.configure
@Resource private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
退出登录接口
LoginServiceImpl
public Ret logout() { // 获取SecurityContextHolder中的用户信息 UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal(); Long userId = loginUser.getUser().getId(); // 删除redis中对应用户的信息 redisUtil.deleteObject("login:" + userId); return Ret.ok().data("退出成功"); }
SecurityConfig
HttpSecurity.antMatchers("/user/login").anonymous()// 未登录可访问,登录不可访问(携带token,500错误)
HttpSecurity.antMatchers("/user/login").permitAll()// 不管是否登录都可以访问
HttpSecurity.anyRequest().authenticated(); // 登陆了就可以访问
授权引入
基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。 在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的
Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入
Authentication。然后设置我们的资源所需要的权限即可。
模拟授权
UserDetailServiceImpl.loadUserByUsername
... // TODO 查询用户权限信息 List<String> list= new ArrayList<>(Arrays.asList("login","admin")); // 把数据封装成UserDetail并返回 return new LoginUser(user,list);
JwtAuthenticationTokenFilter.doFilterInternal
// TODO 获取权限信息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());// authorities表示授权信息 SecurityContextHolder.getContext().setAuthentication(authenticationToken);
配置SecurityConfig
@EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {}
LoginController权限注解,下面表示有hello权限才能访问接口
@PostMapping("/hello") @PreAuthorize("hasAnyAuthority('hello')") public Ret hello(){ ... }
数据库权限与接口访问
数据库表
sec_user
role
user_role
permission
role_permission
根据用户id查询权限sql语句示例
select distinct `p`.`name` from `user_role` ur left join `role_permission` rp on `ur`.role_id = `rp`.role_id left join `permission` p on `p`.id = `rp`.permisson_id where `ur`.user_id = 1 and `p`.`name` is not null -- 根据用户id 查询出角色id,然后根据角色id查询出拥有的权限id,再根据权限id查询出可用菜单集合(去重?用户可用有多个角色身份,可能存在权限重复)
PermissionMapper.java
List<String> selectPermsByUserId(@Param("userId") Long userId);
PermissionMapper.xml
<?xm1 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.learnspringsecurity.mapper.PermissionMapper"> <select id="selectPermsByUserId" resultType="java.lang.String"> select distinct `p`.`name` from `user_role` ur left join `role_permission` rp on `ur`.role_id = `rp`.role_id left join `permission` p on `p`.id = `rp`.permisson_id where `ur`.user_id = #{userId} and `p`.`name` is not null </select> </mapper>
给相应接口定义所需要的权限
@RequestMapping("/hi") @PreAuthorize("hasAnyAuthority('hi')") public String hello(){ return "hi ~ "; }
UserDetailServiceImpl.loadUserByUsername
List<String> list = permissionMapper.selectPermsByUserId(user.getId()); // 把数据封装成UserDetail并返回 return new LoginUser(user,list);
自定义失败处理
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败
还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即呵。
@Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { // 授权失败 异常处理 Ret<String> ret = new Ret<>(HttpStatus.FORBIDDEN.value(),"当前用户没有权限执行该操作",null); String jsonString = JSON.toJSONString(ret); WebUtil.renderString(httpServletResponse, jsonString); } }
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { // 认证失败 异常处理 Ret<String> ret = new Ret<>(HttpStatus.UNAUTHORIZED.value(),"用户认证失败请重新登录",null); String jsonString = JSON.toJSONString(ret); WebUtil.renderString(httpServletResponse, jsonString); } }
SecurityConfig.configure
// 认证/授权 失败处理器 http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler);
跨域
@Configuration public class CrosConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 允许跨域请求 registry.addMapping("/**") // 允许的路径 .allowedOrigins("*") // 允许的域名 .allowCredentials(true) // 允许cookie? .allowedMethods("GET","POST","DELETE","PUT");// 允许的请求方式 } }
SecurityConfig.configure
// 允许跨域 http.cors();
权限校验方法
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations { protected final Authentication authentication; private AuthenticationTrustResolver trustResolver; private RoleHierarchy roleHierarchy; private Set<String> roles; private String defaultRolePrefix = "ROLE_"; public final boolean permitAll = true; public final boolean denyAll = false; private PermissionEvaluator permissionEvaluator; public final String read = "read"; public final String write = "write"; public final String create = "create"; public final String delete = "delete"; public final String admin = "administration"; public SecurityExpressionRoot(Authentication authentication) { if (authentication == null) { throw new IllegalArgumentException("Authentication object cannot be null"); } else { this.authentication = authentication; } } public final boolean hasAuthority(String authority) { return this.hasAnyAuthority(authority); } public final boolean hasAnyAuthority(String... authorities) { return this.hasAnyAuthorityName((String)null, authorities); } public final boolean hasRole(String role) { return this.hasAnyRole(role); } public final boolean hasAnyRole(String... roles) { return this.hasAnyAuthorityName(this.defaultRolePrefix, roles); } private boolean hasAnyAuthorityName(String prefix, String... roles) { Set<String> roleSet = this.getAuthoritySet(); String[] var4 = roles; int var5 = roles.length; for(int var6 = 0; var6 < var5; ++var6) { String role = var4[var6]; String defaultedRole = getRoleWithDefaultPrefix(prefix, role); if (roleSet.contains(defaultedRole)) { return true; } } return false; } public final Authentication getAuthentication() { return this.authentication; } public final boolean permitAll() { return true; } public final boolean denyAll() { return false; } public final boolean isAnonymous() { return this.trustResolver.isAnonymous(this.authentication); } public final boolean isAuthenticated() { return !this.isAnonymous(); } public final boolean isRememberMe() { return this.trustResolver.isRememberMe(this.authentication); } public final boolean isFullyAuthenticated() { return !this.trustResolver.isAnonymous(this.authentication) && !this.trustResolver.isRememberMe(this.authentication); } public Object getPrincipal() { return this.authentication.getPrincipal(); } public void setTrustResolver(AuthenticationTrustResolver trustResolver) { this.trustResolver = trustResolver; } public void setRoleHierarchy(RoleHierarchy roleHierarchy) { this.roleHierarchy = roleHierarchy; } public void setDefaultRolePrefix(String defaultRolePrefix) { this.defaultRolePrefix = defaultRolePrefix; } private Set<String> getAuthoritySet() { if (this.roles == null) { Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities(); if (this.roleHierarchy != null) { userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities); } this.roles = AuthorityUtils.authorityListToSet(userAuthorities); } return this.roles; } public boolean hasPermission(Object target, Object permission) { return this.permissionEvaluator.hasPermission(this.authentication, target, permission); } public boolean hasPermission(Object targetId, String targetType, Object permission) { return this.permissionEvaluator.hasPermission(this.authentication, (Serializable)targetId, targetType, permission); } public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { this.permissionEvaluator = permissionEvaluator; } private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) { if (role == null) { return role; } else if (defaultRolePrefix != null && defaultRolePrefix.length() != 0) { return role.startsWith(defaultRolePrefix) ? role : defaultRolePrefix + role; } else { return role; } } }
自定义权限校验方法
@Component("customExpressionRoot") public class CustomExpressionRoot { public boolean hasAuthority(String authority){ // 获取当前用户权限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); // 判断是否存在对应权限 return permissions.contains(authority); } }
在SPEL表达式中使用@ex相当于获取容器中bean的名字为customExpressionRoot的对象。然后再调用这个对象的hasAuthority方法
@RequestMapping("/custom") @PreAuthorize("@customExpressionRoot.hasAuthority('custom')") public Ret custom(){ return Ret.ok(); }
通过配置文件
接口路径与所需权限
HttpSecurity.antMatchers("/hi").hasAuthority("hi")
CSRF
CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
https://blog.csdn.net/freeking101/article/details/86537087
SpringSecurity却防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf. token,前端发起请求的时候需要携带这个csrf_ token,后端会有过滤器进行校验,
如果没有携带或者是伪造的就不允许访问。我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,
而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
认证成功处理
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } private AuthenticationSuccessHandler authenticationSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().successHandler(authenticationSuccessHandler); http.authorizeRequests().anyRequest().authenticated(); } /** * 作为bean放入容器中 * @return AuthenticationManager * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
@Component public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { System.out.println("认证成功了"); System.out.println(authentication.getPrincipal().toString()); } }
当提交这个表单
输出
认证成功了 LoginUser(...
认证失败处理器
@Component public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { System.out.println("认证失败了"); } }
... @Resource private AuthenticationFailureHandler authenticationFailureHandler; ... HttpSecurity.failureHandler(authenticationFailureHandler);
登出成功处理器
HttpSecurity.logout().logoutSuccessHandler(logoutSuccessHandler);
@Component public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { System.out.println("登出成功"); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?