一 引言
在基于springsecurity和jwt实现的单体项目token认证中我实现了基于jwt实现的认证,本文在此基础上继续实现权限认证
- 用户认证成功后携带jwt发起请求,请求被
AuthenticationFilter
拦截到,进行jwt的校验 - jwt校验成功后,调用
JwtAuthenticationProvider
从jwt中获得权限信息,加载到Authcation中 - 将Authcation加载安全上下文
SecurityContextHolder
FilterSecurityInterceptor
从上下文中获得用户权限信息,根据校验规则进行用户数据的权限校验
二 代码实现
用户认证成功生成jwt时将权限信息加载到jwt中
package com.xlcp.xlcpdemo.auth.token;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.JWTValidator;
import cn.hutool.jwt.RegisteredPayload;
import cn.hutool.jwt.signers.AlgorithmUtil;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.xlcp.xlcpdemo.auth.common.AccessToken;
import com.xlcp.xlcpdemo.auth.common.AccessTokenType;
import com.xlcp.xlcpdemo.auth.common.AuthProperties;
import com.xlcp.xlcpdemo.auth.core.AccessTokenManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
/**
* @author likun
* @date 2022年07月12日 13:48
*/
@Slf4j
public class JwtAccessTokenManager implements AccessTokenManager {
private final AuthProperties authProperties;
private final JWTSigner jwtSigner;
// 省略....
@Override
public AccessToken createToken(Authentication authentication) {
AccessToken accessToken = new AccessToken();
accessToken.setTokenType(AccessTokenType.JWT.name());
accessToken.setExpireInTimeMills(authProperties.getExpireInTimeMills());
HashMap<String, Object> payloads = new HashMap<String, Object>();
payloads.put(RegisteredPayload.AUDIENCE, authentication.getName());
payloads.put(RegisteredPayload.JWT_ID, IdUtil.fastUUID());
DateTime expiredAt = DateUtil.offset(new Date(), DateField.MILLISECOND, Convert.toInt(authProperties.getExpireInTimeMills()));
payloads.put(RegisteredPayload.EXPIRES_AT, expiredAt);
// todo 数据库查询权限信息
List permissions = CollUtil.newArrayList("ROLE_BUYER","ROLE_SELLER","user_find_account");
payloads.put("authDetails", permissions);
String token = JWTUtil.createToken(payloads, this.jwtSigner);
accessToken.setAccessToken(token);
return accessToken;
}
}
定义
JwtAuthenticationProvider
和JwtAuthenticationToken
用于认证成功从jwt中解析jwt中的权限信息
package com.xlcp.xlcpdemo.auth.core;
import cn.hutool.jwt.JWT;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import java.util.List;
/**
* @author likun
* @date 2022年12月01日 12:25
*/
public class JwtAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
String token = jwtAuthenticationToken.getToken();
JWT jwt = JWT.create().parse(token);
Object authDetails = jwt.getPayload("authDetails");
Object aud = jwt.getPayload("aud");
List<GrantedAuthority> permissions;
if (authDetails!=null&&authDetails instanceof List){
List<String> auths = (List<String>) authDetails;
permissions=AuthorityUtils.createAuthorityList(auths.toArray(new String[0]));
}else {
permissions = AuthorityUtils.createAuthorityList("");
}
return new JwtAuthenticationToken(aud,null,permissions);
}
@Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
}
package com.xlcp.xlcpdemo.auth.core;
import lombok.Getter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* @author likun
* @date 2022年12月01日 11:51
*/
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private Object principal;
private Object credentials;
@Getter
private String token;
public JwtAuthenticationToken(Object principal,Object credentials,Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal= principal;
this.credentials= credentials;
setAuthenticated(true);
}
public JwtAuthenticationToken(String token){
super(null);
this.token=token;
setAuthenticated(false);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
}
jwt校验成功后解析jwt并加载到安全上下文中
package com.xlcp.xlcpdemo.auth.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Header;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.RegisteredPayload;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.xlcp.xlcpdemo.auth.common.AuthProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
import static com.xlcp.xlcpdemo.entity.PtUser.ACCOUNT;
/**
* @author likun
* @date 2022年07月12日 15:14
*/
@Slf4j
public class AuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER = "bearer";
private final AuthProperties authProperties;
private final AccessTokenManager accessTokenManager;
private final AntPathMatcher antPathMatcher;
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthProperties authProperties, AccessTokenManager accessTokenManager, AntPathMatcher antPathMatcher,AuthenticationManager authenticationManager){
this.authProperties=authProperties;
this.accessTokenManager=accessTokenManager;
this.antPathMatcher=antPathMatcher;
this.authenticationManager=authenticationManager;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 判断当前请求是否为忽略的路径
Set<String> ignorePaths = authProperties.getIgnorePaths();
if (CollUtil.isNotEmpty(ignorePaths)){
for (String ignorePath : ignorePaths) {
if (antPathMatcher.match(ignorePath,request.getRequestURI())){
filterChain.doFilter(request, response);
return;
}
}
}
// token校验
String bearerToken = request.getHeader(Header.AUTHORIZATION.getValue());
if (StrUtil.isBlank(bearerToken)){
response.setStatus(HttpStatus.UNAUTHORIZED.value());
throw new InsufficientAuthenticationException("unauthorized request.");
}
final String accessToken = bearerToken.trim().substring(BEARER.length()).trim();
boolean valid = false;
try {
valid = accessTokenManager.verify(accessToken);
} catch (Exception e) {
log.warn("verify access token [{}] failed.", accessToken);
throw new InsufficientAuthenticationException("invalid access token + [ " + accessToken + " ].");
}
if (!valid) {
throw new InsufficientAuthenticationException("invalid access token + [ " + accessToken + " ].");
}
final String account = request.getParameter(ACCOUNT);
if (StringUtils.isBlank(account)) {
SetAuthentication(accessToken);
filterChain.doFilter(request, response);
return;
}
//校验是否本人
final String audience = JWT.of(accessToken).getPayload(RegisteredPayload.AUDIENCE).toString();
if (!account.equalsIgnoreCase(audience)) {
throw new AccessDeniedException("invalid account. parameter [ " + account + " ]. account in token [ " + audience + " ].");
}
SetAuthentication(accessToken);
filterChain.doFilter(request, response);
}
// 解析jwt并加载到安全上下文中
private void SetAuthentication(String accessToken) {
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(accessToken);
Authentication authenticate = authenticationManager.authenticate(jwtAuthenticationToken);
SecurityContextHolder.getContext().setAuthentication(authenticate);
}
}
自定义权限不足返回异常处理
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpStatus.FORBIDDEN.value());
R<Object> result = R.failed("无访问权限");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(result));
}
}
完成相应的配置
二 权限访问
2.1 理论基础
Spring Security是一个功能强大且高度可定制的
身份认证
和访问控制
框架,它是保护基于spring应用程序的事实标准。
权限访问:就是 给用户角色添加角色权限,使得不同的用户角色只能访问特定的接口资源
,对于其他接口无法访问
2.2 权限分类
根据业务的不同将权限的控制分为两类,一类是
To-C简单角色的权限控制
,一类是To-B基于RBAC数据模型的权限控制
。
- To-C简单角色的权限控制
例如
买家和卖家
,这两者都是单独的个体,一般来说都是只有一种独立的角色,比如卖家角色:ROLE_SELLER
,买家角色:ROLE_BUYER
。这类一般比较粗粒度的将角色划分,且角色比较固定,角色拥有的权限也是比较固定
,在项目启动的时候就固定了。
- To-B基于RBAC数据模型的权限控制
例如
PC后台的管理端
,能登录的是企业的人员,企业人员可以有不同的角色,角色的权限也可以比较随意地去改变
,比如总经理角色可以访问所有资源,店铺管理人员只能访问店铺和卖家相关信息,会员管理人员可以访问买家相关信息等等,这时候就可以使用基于RBAC数据模型结合Spring Security的访问控制来实现权限方案。这类一般角色划分较细,角色的权限也是上线后在PC端可任意配置
。
在我们的日常开发中一般用得比较多的是第二种
2.3 To-C:简单角色的权限控制
定义相应的接口
@RestController
@RequestMapping("/buyer")
public class BuyerController {
/**
* 买家下订单
*
* @return
*/
@GetMapping("/order:create")
public String receiveOrder() {
return "买家下单啦!";
}
/**
* 买家订单支付
*
* @return
*/
@GetMapping("/order:pay")
public String deliverOrder() {
return "买家付款了!";
}
}
@RestController
@RequestMapping("/seller")
public class SellerController {
/**
* 卖家接单
*
* @return
*/
@GetMapping("/order:receive")
@Secured("ROLE_SELLER")
public String receiveOrder() {
return "卖家接单啦!";
}
/**
* 卖家订单发货
*
* @return
*/
@GetMapping("/order:deliver")
@Secured("ROLE_SELLER")
public String deliverOrder() {
return "卖家发货啦!";
}
}
我们要做到的是,买家角色只拥有买家接口权限,卖家角色只拥有卖家接口权限。而关于配置角色权限有两种实现方式,一种是在
核心配置类中统一配置
(买家角色演示),还有一种是在接口上以注解的方式配置
(卖家角色演示)。
2.3.1 统一配置
在核心配置类(WebSecurityConfig
)中,统一配置买家角色权限,角色名称是 ROLE_BUYER,拥有访问 /buyer/** 接口的权限。
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.authorizeRequests()
.antMatchers("/buyer/**").hasRole("BUYER")
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.addFilterBefore(authenticationFilter(accessTokenManager()), UsernamePasswordAuthenticationFilter.class);
}
2.3.2 注解方式
可以使用注解的方式配置接口所能访问的角色,比如卖家端两个接口配置了 ROLE_SELLER 角色才能访问
@RestController
@RequestMapping("/seller")
public class SellerController {
/**
* 卖家接单
*
* @return
*/
@GetMapping("/order:receive")
@Secured("ROLE_SELLER")
public String receiveOrder() {
return "卖家接单啦!";
}
/**
* 卖家订单发货
*
* @return
*/
@GetMapping("/order:deliver")
@Secured("ROLE_SELLER")
public String deliverOrder() {
return "卖家发货啦!";
}
}
@Secured、@RolesAllowed、@PreAuthorize
注解都可以达到这样的效果,所有注解能发挥有效的前提是需要在核心配置类加上注解 @EnableGlobalMethodSecurity
,然后在此注解上启用对应的注解配置方式,注解才能生效,否则无法起作用,比如要使 @Secured 注解生效需要配置@EnableGlobalMethodSecurity(securedEnabled = true)
注解能否生效和启用注解的属性对应关系如下,简单解释就是要使接口上的注解生效,就需要在核心过滤器配置注解 @EnableGlobalMethodSecurity
,然后启用注解对应的属性,就是将属性值设为true。
生效注解 | 启用注解的属性 | 核心配置器上注解配置 |
---|---|---|
@Secured | securedEnabled | @EnableGlobalMethodSecurity(securedEnabled = true) |
@RolesAllowed | jsr250Enabled | @EnableGlobalMethodSecurity(jsr250Enabled= true) |
@PreAuthorize | prePostEnabled | @EnableGlobalMethodSecurity(prePostEnabled = true) |
2.3.3 测试
只设置ROLE_BUYER
角色
买家能正常访问
卖家无访问权限
三 To-B:基于RBAC数据模型的权限控制
RBAC数据模型
- 全称:
Role-Based Access Control
(基于角色的访问控制) - 一般会有五个表组成,
三张主体表(用户、角色、权限),两张关联表(用户-角色、角色-权限)
。
3.1 案例
首先关于RBAC的数据模型大家应该都很熟悉,这里不再创建,即不会涉及到存储。其实这一类相对上面那类区别在于这类的权限不是固定的,需要实时的重新查询出来,再进行判断请求是否有权访问,所以判断
是否有权访问的逻辑
需要自己完善,写好之后再配置进框架中即可。
申明权限校验基础接口
public interface PermissionService {
/**
* 判断是否拥有权限
* @param permissions
* @return
*/
boolean hasPermission(String... permissions);
}
@Component("pms")
public class PermissionServiceImpl implements PermissionService{
@Override
public boolean hasPermission(String... permissions) {
if (ArrayUtil.isEmpty(permissions)){
return false;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
return authorities.stream().map(GrantedAuthority::getAuthority).filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
}
}
在相应的接口上申明相应的权限
开启相应权限支持
用户认证时查询数据库 加载不同的权限
没有权限查询结果
有权限时查询结果
四 权限表达式
上面.permitAll()、.hasRole()、.access()
表示权限表达式,而权限表达式实际上都是 Spring中强大的Spel表达式,如下还有很多可以使用的权限表达式以及和Spel表达式的转换关系
权限表达式(ExpressionUrlAuthorizationConfigurer) | 说明 | Spel表达式 | Spel表达式实际执行方法(SecurityExpressionOperations) |
---|---|---|---|
permitAll() | 表示允许所有,永远返回true | permitAll | permitAll() |
denyAll() | 表示拒绝所有,永远返回false | denyAll | denyAll() |
anonymous() | 当前用户是anonymous时返回true | anonymous | isAnonymous() |
rememberMe() | 当前用户是rememberMe用户时返回true | rememberMe | isRememberMe() |
authenticated() | 当前用户不是anonymous时返回true | authenticated | isAuthenticated() |
fullyAuthenticated() | 当前用户既不是anonymous也不是rememberMe用户时返回true | fullyAuthenticated | isFullyAuthenticated() |
hasRole(“BUYER”) | 用户拥有指定权限时返回true | hasRole(‘ROLE_BUYER’) | hasRole(String role) |
hasAnyRole(“BUYER”,“SELLER”) | 用于拥有任意一个角色权限时返回true | hasAnyRole (‘ROLE_BUYER’,‘ROLE_BUYER’) | hasAnyRole(String… roles) |
hasAuthority(“BUYER”) | 同hasRole | hasAuthority(‘ROLE_BUYER’) | hasAuthority(String role) |
hasAnyAuthority(“BUYER”,“SELLER”) | 同hasAnyRole | hasAnyAuthority (‘ROLE_BUYER’,‘ROLE_BUYER’) | hasAnyAuthority(String… authorities) |
hasIpAddress(‘192.168.1.0/24’) | 请求发送的Ip匹配时返回true | hasIpAddress(‘192.168.1.0/24’) | hasIpAddress(String ipAddress),该方法在WebSecurityExpressionRoot类中 |
access(“@rbacService.hasPermission(request, authentication)”) | 可以自定义Spel表达式 | @rbacService.hasPermission (request, authentication) | hasPermission(request, authentication) ,该方法在自定义的RbacServiceImpl类中 |