一、发生的问题
- oauth登录模块登录成功后,带着token去访问带权限的接口,发生 Spring Security: 不允许访问 的问题
- 查看权限断点发现是携带的权限被多加了一段字符串
前提:
- 自定义实现UserDetailsService 加载用户信息和权限 到UserDetails对象
package com.lxy.blog.config;
import com.lxy.blog.service.api.UserFeignClient;
import domain.SecurityUserDetails;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import util.BeanUtil;
/**
*@author liuxingying
* 这个类就是用户来请求获取token时,根据该用户的username加载用户信息返回即可,
* 而校验用户账号密码的操作之后会被AuthenticationManager调用一个认证器去校验用户账号密码完成
*@since 2020/12/31
*/
@Slf4j
@Service("ouath2UserDetailService")
public class Ouath2UserDetailService implements UserDetailsService {
@Autowired
private UserFeignClient userFeignClient;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Assert.notNull(username,"aut username can not be null");
// (省略.......)创建UserTokenVo,它就是 要继承UserDetails接口,添加稍后要存放到token里的字段到这个类(稍后自定义实现token增强器),
/* SecurityUserDetails user = new SecurityUserDetails();
user.setUserId("001");
user.setNickName("lxy");
user.setPassword("$2a$10$KjvPVs6ast3f34CfKQR8m.QfUVqfPE5oBrzke.asucA/v7EN4y49O");*/
return checkEmpty(userFeignClient.loadUserByUsername(username));
}
/**
*
* null 处理
*
* @author liuxingying
* @since 2020/12/31
**/
private SecurityUserDetails checkEmpty(SecurityUserDetails securityUserDetails) {
if (securityUserDetails == null){
return null;
}
return securityUserDetails;
}
}
@Override
public SecurityUserDetails loadUserByUsername(String username) {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getName, username));
if (ObjectUtils.isEmpty(user)){
throw BusinessException.busExp(MemberExceptionCode.BIZ_ACCOUNT_UNREGISTERED);
}
SecurityUserDetails userDetails = new SecurityUserDetails();
userDetails.setUsername(user.getName());
userDetails.setNickName(user.getNickname());
userDetails.setPassword(user.getPassword());
// TODO 用户权限 暂设假数据
List<SecurityAuthority> list = new ArrayList<>();
SecurityAuthority authRole = new SecurityAuthority();
authRole.setAuthority("read");
list.add(authRole);
userDetails.setAuthorities(list);
//userDetails.setAuthorities(Collections.singletonList(authRole));
return userDetails;
}
- 访问权限接口:
@ApiOperation(value = "根据id查询", notes = "")
@GetMapping("/info")
@PreAuthorize("hasAuthority('read')")
public Result<User> getByInfo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User lxy = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getName, "lxy"));
return new Result<>(lxy);
}
二、问题来了:
Spring Security: 不允许访问,为什么会出现这样的问题呢?我明明携带的是"read"权限,但是读取却是"authority=read"
- 首先我们来简单看看oauth是怎么运作的,请求登录接口时,将有两行代码在权限管理器中进行获取用户基本信息和权限,并设置到SecurityContextHolder全文中
- Authentication authentication = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
public OAuth2AccessToken checkToken(HttpServletRequest request, AbstractAuthenticationToken token, String grantType) {
try {
String clientId = request.getHeader("client_id");
String clientSecret = request.getHeader("client_secret");
if (StringUtils.isEmpty(clientId)) {
throw new UnapprovedClientAuthenticationException("请求头中无client_id信息");
}
if (StringUtils.isEmpty(clientSecret)) {
throw new UnapprovedClientAuthenticationException("请求头中无client_secret信息");
}
ClientDetails clientDetails = getClient(clientId, clientSecret, null);
TokenRequest tokenRequest = new TokenRequest(new HashMap<>(), clientId, clientDetails.getScope(), grantType);
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
Authentication authentication = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
oAuth2Authentication.setAuthenticated(true);
return oAuth2AccessToken;
} catch (AuthenticationException e){
log.error("登录失败!", e.getMessage());
int code = 400401;
String msg = ExceptionCode.SYS_SERVICE_BUSY.getMessage();
if (e instanceof LockedException) {
msg = "账户被锁定,请联系管理员!";
code = 400402;
} else if (e instanceof CredentialsExpiredException) {
msg = "密码过期,请联系管理员!";
code = 400403;
} else if (e instanceof AccountExpiredException) {
msg = "账户过期,请联系管理员!";
code = 400404;
} else if (e instanceof DisabledException) {
msg = "账户被禁用,请联系管理员!";
code = 400405;
} else if (e instanceof BadCredentialsException) {
msg = "账号或者密码输入错误,请重新输入!";
code = 400406;
} else if (e instanceof InternalAuthenticationServiceException) {
msg = "账号不存在,请重新输入!";
code = 400407;
} else if (e instanceof LoginAuthenticationServiceException){
LoginAuthenticationServiceException ex = (LoginAuthenticationServiceException)e;
throw new BusinessException(ex.getCode(), ex.getMessage());
}
throw new BusinessException(code, msg);
}
- 在这过程中会将自定义的OAuth2AccessToken封装好并且设置到tokenStore中,并且返回给前端做展示
/**
*@author liuxingying
*@description
*@since 2020/12/31
*/
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
// 这个UserTokenVo就是之前UserDetial返回的对象
//从那获取要增强携带的字段
SecurityUserDetails user = (SecurityUserDetails) authentication.getPrincipal();
final Map<String, Object> additionalInfo = new HashMap<>();
//添加token携带的字段
// additionalInfo.put("userId", user.getUserId());
additionalInfo.put("nickname", user.getNickName());
additionalInfo.put("authorities", user.getAuthorities());
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
token.setAdditionalInformation(additionalInfo);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
问题就出在这儿:我想将用户的权限列表也返回给前端,就出现了后续的bug
- 为什么呢?因为 additionalInfo.put(“authorities”, user.getAuthorities());前面讲了,自定义的OAuth2AccessToken封装好会被设置到tokenStore中,源码如下
- 登录成功会携带OAuth2AccessToken对象返回前端
- 此时拿到token后我们就用token去请求需要权限校验的接口,在oauth底层此时会先将token带入tokenStore中进行处理取得权限对象
- 我是用的jwt,所以会进行decode解析token
- 将token中解析后的map传入DefaultAccessTokenConverter默认访问令牌转换器中
执行方法:Authentication user = userTokenConverter.extractAuthentication(map);
执行方法:Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
- 注意:此时就出现问题了,我之前是将权限的列表作为map的值封装到了Oauth2token对象中,此时就会去获取map值,如果没有就取默认的登录时自定义的SecurityUserDetails 对象中的 authorities权限列表值 private List authorities;
但是此时我map的key同名了,也写作了authorities,所以就从map中取,然后用工具方法去把列表转换了,但是我map的authorities对应的value是一个对象,直接给我转换了对象属性=值,导致我再权限接口进行权限对比的时候对应不上
三、总结
所以直接去掉自定义map的权限信息的或者将key名字换成与authorities不一样的就可以了,建议还是去掉,我是手残加上然后就看了半天源码才看到问题。