最近在复习Spring Security,复习的鉴权的时候出现问题。26.封装权限信息_哔哩哔哩_bilibili
如果是从B站中看到,直接说问题可能出现的原因:可能是private List
下图是代码中aaaaa是我测试的权限信息集合,故意命名写的不规范,才知道原因。
问题场景:使用Spring Security模拟授权的时候,手动存入几个权限信息,然后正确的访问时出现了问题。
重要的事情放前面说:如果不配置权限集合如果不加@JSONField(serialize = false)注解,解析的时候会报错。
UserDetailServiceImpl.java UserDetailsService实现类
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper queryChainWrap = new LambdaQueryWrapper<>();
queryChainWrap.eq(!Objects.isNull(username), User::getUserName, username);
User user = userMapper.selectOne(queryChainWrap);
// 如果没有查询到用户就抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
//TODO 查询对应的权限信息
ArrayList list = new ArrayList<>(Arrays.asList("test", "admin"));
// 把数据封装成UserDetails返回
return new LoginUser(user, list);
// return new LoginUser(user);
}
}
LoginServiceImpl.java 自定义登录接口实现类
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) throws Exception {
//AuthenticationManager authenticate进行用户认证
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
// authenticate.getPrincipal()
// 如果认证没有通过,给出对应的提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
// 如果认证通过了,使用userid生成jwt jwt存入ResponseResult返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
Map map = new HashMap<>();
map.put("token", jwt);
// 把完整的用户信息存入redis userid作为key
redisCache.setCacheObject(SystemConstants.LOGIN_TOKEN_PREFIX + userId, loginUser);
return new ResponseResult(200, "登录成功", map);
}
@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(SystemConstants.LOGIN_TOKEN_PREFIX + userId);
return new ResponseResult(200, "注销成功");
}
}
JwtAuthenticationTokenFilter.java 自定义过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Autowired
private AuthenticationManager authenticationManager;
@SneakyThrows(value = {RuntimeException.class, ArrayIndexOutOfBoundsException.class, Exception.class})
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
// token为内容为空或者不以login:开头直接放行
if (!StringUtils.hasText(token)) {
// 放行
filterChain.doFilter(request, response);
return;
}
// 解析token获取其中的userId
String userId;
try {
userId = JwtUtil.parseJWT(token).getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// 从redis中获取用户信息
String redisKey = SystemConstants.LOGIN_TOKEN_PREFIX + userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
// 存入SecurityContextHolder
// TODO 获取权限信息封装到Authentication
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
// 这里不知道为什么加这句,导致进行验证,走了后续过滤器,但是我发送的测试请求是带token的get请求,
// 但是我的数据是从redis中获取,密码是加密后的,导致二次加密后,密码不一致
// Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
待测试的Controller
LoginUser.java UserDetails实现类
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private List permission;
@JSONField(serialize = false)
// @JsonIgnore
// @JsonIgnoreProperties
private List aaaaa;
// @JSONField(serialize = false)
// private List authorities;
public LoginUser(User user, List permission) {
this.user = user;
this.permission = permission;
}
public LoginUser(User user) {
this.user = user;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
// 把permission中String类型的去啊年信息封装成SimpleGrantedAuthority对象
if (Objects.isNull(aaaaa)) {
synchronized (this) {
if (Objects.isNull(aaaaa)) {
aaaaa
= permission.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
}
}
return aaaaa;
// ArrayList list = new ArrayList<>();
// list.add(new SimpleGrantedAuthority("Powerveil"));
// return list;
}
@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;
}
}
直接看结果
没有任何输出
看一下idea的console
2023-06-18 18:46:34.557 ERROR 21216 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
com.alibaba.fastjson.JSONException: autoType is not support. org.springframework.security.core.authority.SimpleGrantedAuthority
at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:830) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:596) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:226) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:222) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.DefaultJSONParser.parseArray(DefaultJSONParser.java:716) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.serializer.CollectionCodec.deserialze(CollectionCodec.java:120) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:78) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField(JavaBeanDeserializer.java:911) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:656) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:226) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:222) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:357) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1325) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.deserializer.JavaObjectDeserializer.deserialze(JavaObjectDeserializer.java:45) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:630) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.JSON.parseObject(JSON.java:354) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.JSON.parseObject(JSON.java:258) ~[fastjson-1.2.33.jar:na]
at com.alibaba.fastjson.JSON.parseObject(JSON.java:471) ~[fastjson-1.2.33.jar:na]
at com.powerveil.utils.FastJsonRedisSerializer.deserialize(FastJsonRedisSerializer.java:56) ~[classes/:na]
at org.springframework.data.redis.core.AbstractOperations.deserializeValue(AbstractOperations.java:335) ~[spring-data-redis-2.5.1.jar:2.5.1]
at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:61) ~[spring-data-redis-2.5.1.jar:2.5.1]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:222) ~[spring-data-redis-2.5.1.jar:2.5.1]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:189) ~[spring-data-redis-2.5.1.jar:2.5.1]
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96) ~[spring-data-redis-2.5.1.jar:2.5.1]
at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:53) ~[spring-data-redis-2.5.1.jar:2.5.1]
at com.powerveil.utils.RedisCache.getCacheObject(RedisCache.java:78) ~[classes/:na]
at com.powerveil.filter.JwtAuthenticationTokenFilter.doFilterInternal(JwtAuthenticationTokenFilter.java:61) ~[classes/:na]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.7.jar:5.3.7]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.7.jar:5.3.7]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:110) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:80) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.7.jar:5.3.7]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183) ~[spring-security-web-5.5.0.jar:5.5.0]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358) ~[spring-web-5.3.7.jar:5.3.7]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271) ~[spring-web-5.3.7.jar:5.3.7]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.7.jar:5.3.7]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.7.jar:5.3.7]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.7.jar:5.3.7]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.7.jar:5.3.7]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.7.jar:5.3.7]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.7.jar:5.3.7]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.46.jar:9.0.46]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_321]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_321]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.46.jar:9.0.46]
at java.lang.Thread.run(Thread.java:750) [na:1.8.0_321]
错误日志多不要害怕
autoType is not support 不支持自动类型
看下面的错误
RedisCache.java是一个自定义的Redis工具类
原来是redis取出数据出现问题,看一下redis中存储的数据。
{
"@type": "com.powerveil.domain.LoginUser",
"accountNonExpired": true,
"accountNonLocked": true,
"authorities": [
{
"@type": "org.springframework.security.core.authority.SimpleGrantedAuthority",
"authority": "test"
},
{
"@type": "org.springframework.security.core.authority.SimpleGrantedAuthority",
"authority": "admin"
}
],
"credentialsNonExpired": true,
"enabled": true,
"password": "$2a$10$4MzpJkpyo2gywu5B/b6zEO3gfyYqcEN.6PG5wEpcb9nASzPddq7fu",
"permission": [
"test",
"admin"
],
"user": {
"delFlag": 0,
"email": "[email protected]",
"id": 1,
"nickName": "Powerveil",
"password": "$2a$10$4MzpJkpyo2gywu5B/b6zEO3gfyYqcEN.6PG5wEpcb9nASzPddq7fu",
"phonenumber": "123456789",
"sex": "0",
"status": "0",
"userName": "Powerveil",
"userType": "1"
},
"username": "Powerveil"
}
看到redis数据,怎么这么多字段
我自定义了只有三个字段
user
permission
aaaaa
user,permisssion都看到出来,但是我的aaaaa怎么没有了,正常,因为使用了@JSONField(serialize = false)注解,不会写入redis中。
postman返回的结果
上面是我第一次遇到的情况,以下是其他测试情况,看完你就知道原因了。
2.将getAuthorities方法的返回值设为null其他不变。
redis中的结果
发现authorities消失了
数据可以正常解析
但是结果403,但是也正常,因为鉴权的方法的返回值是null。
3.添加authorities字段
方法正常重写
查看redis数据
发现authorities字段生成
redis数据解析错误
postman返回的结果
4.给authorities字段加上@JSONField(serialize = false)注解
查看redis数据,发现authorities字段消失
可以获取正常解析redis数据到loginUser
可以输出结果
我推测是Spring Security会判断authorities字段是否存在如果getAuthorities方法返回值为null或者该字段加上@JSONField(serialize = false),则不会存入redis中。如下图
其他情况会存入redis中。如下图
但是我们最后要做的是鉴权的时候,方法的返回值不能是null呀。所以只有给authorities字段加上@JSONField(serialize = false)注解才能解决问题。现在突然醒悟了,那我们还要aaaaa这个字段干什么,直接加上authorities字段再给它配个注解,方法里面都写authorities不就解决问题了?是的,确实是这样,因为Spring Security检查的是authorities字段加上@JSONField(serialize = false)注解才会判断要不要写入redis。这个aaaaa这个字段确实多余。
看到这里,大家可能知道问题的原因了,是因为没有定义authorities字段(不能写错!!!)。
查看效果
redis中的数据
redis解析正常
postman返回数据正常
思考:LoginUser类中字段只写了三个,而且一个加入注解忽略写入redis,为什么还有这么多其他字段?
看一下LoginUser的方法
像不像方法与字段像对应
这样就清晰了,getAuthorities方法应该也可以对应一个字段authorites,如果返回值不为null那个就会有生成字段。存入redis的时候发现这个生成的authorites字段,因为是生成的,没有配置@JSONField(serialize = false)注解,所以会直接存入redis。取出的时候发现没有这个字段,就会报错,就算有这个字段,也解析不出来(重要的事情在文章场景描述的地方)。
这个提醒了我们什么?要规范命名(手动狗头)