Spring Security基于jwt实现权限校验
创始人
2024-03-07 09:17:57
0

一 引言

在基于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;// 省略....@Overridepublic AccessToken createToken(Authentication authentication) {AccessToken accessToken = new AccessToken();accessToken.setTokenType(AccessTokenType.JWT.name());accessToken.setExpireInTimeMills(authProperties.getExpireInTimeMills());HashMap payloads = new HashMap();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;}}

定义JwtAuthenticationProviderJwtAuthenticationToken用于认证成功从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 {@Overridepublic 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 permissions;if (authDetails!=null&&authDetails instanceof List){List auths = (List) authDetails;permissions=AuthorityUtils.createAuthorityList(auths.toArray(new String[0]));}else {permissions = AuthorityUtils.createAuthorityList("");}return new JwtAuthenticationToken(aud,null,permissions);}@Overridepublic 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;@Getterprivate String token;public JwtAuthenticationToken(Object principal,Object credentials,Collection authorities) {super(authorities);this.principal= principal;this.credentials= credentials;setAuthenticated(true);}public JwtAuthenticationToken(String token){super(null);this.token=token;setAuthenticated(false);}@Overridepublic Object getCredentials() {return credentials;}@Overridepublic 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;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 判断当前请求是否为忽略的路径Set 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 {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setStatus(HttpStatus.FORBIDDEN.value());R 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。

生效注解启用注解的属性核心配置器上注解配置
@SecuredsecuredEnabled@EnableGlobalMethodSecurity(securedEnabled = true)
@RolesAllowedjsr250Enabled@EnableGlobalMethodSecurity(jsr250Enabled= true)
@PreAuthorizeprePostEnabled@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{@Overridepublic boolean hasPermission(String... permissions) {if (ArrayUtil.isEmpty(permissions)){return false;}Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication == null) {return false;}Collection 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()表示允许所有,永远返回truepermitAllpermitAll()
denyAll()表示拒绝所有,永远返回falsedenyAlldenyAll()
anonymous()当前用户是anonymous时返回trueanonymousisAnonymous()
rememberMe()当前用户是rememberMe用户时返回truerememberMeisRememberMe()
authenticated()当前用户不是anonymous时返回trueauthenticatedisAuthenticated()
fullyAuthenticated()当前用户既不是anonymous也不是rememberMe用户时返回truefullyAuthenticatedisFullyAuthenticated()
hasRole(“BUYER”)用户拥有指定权限时返回truehasRole(‘ROLE_BUYER’)hasRole(String role)
hasAnyRole(“BUYER”,“SELLER”)用于拥有任意一个角色权限时返回truehasAnyRole (‘ROLE_BUYER’,‘ROLE_BUYER’)hasAnyRole(String… roles)
hasAuthority(“BUYER”)同hasRolehasAuthority(‘ROLE_BUYER’)hasAuthority(String role)
hasAnyAuthority(“BUYER”,“SELLER”)同hasAnyRolehasAnyAuthority (‘ROLE_BUYER’,‘ROLE_BUYER’)hasAnyAuthority(String… authorities)
hasIpAddress(‘192.168.1.0/24’)请求发送的Ip匹配时返回truehasIpAddress(‘192.168.1.0/24’)hasIpAddress(String ipAddress),该方法在WebSecurityExpressionRoot类中
access(“@rbacService.hasPermission(request, authentication)”)可以自定义Spel表达式@rbacService.hasPermission (request, authentication)hasPermission(request, authentication) ,该方法在自定义的RbacServiceImpl类中

相关内容

热门资讯

初中记叙文:那双紧握的手【推... 初中记叙文:那双紧握的手 篇一我记得那是一个寒冷的冬日,我和妈妈一起去参加一个社区活动。活动结束后,...
父亲初一作文【精简6篇】 父亲初一作文 篇一:我和爸爸的足球之旅爸爸是我的英雄,他是一位充满活力和热情的足球迷。每当有重要的足...
初一作文我的爱好800字记叙... 初一作文我的爱好800字记叙文 第一篇大家好!我是一个非常活泼可爱,并且爱好广泛的小女孩。我的业余爱...
最底层的人初一作文【推荐5篇... 最底层的人初一作文 篇一最底层的人初一作文我是一个来自最底层的人,我是一个农村的孩子。在我心中,最底...
新学期的一天初一作文【推荐3... 新学期的一天初一作文 篇一初一的新学期,阳光明媚,充满了希望和憧憬。我早早地起床,洗漱完毕,穿上整洁...
妈妈初一作文(精彩6篇) 妈妈初一作文 篇一:妈妈是我的英雄妈妈是我的英雄。她是一个充满爱心和勇气的人,她总是尽力为我们创造一...
教我如何不想她作文600字(... 教我如何不想她作文600字 篇一教我如何不想她我们常常会遇到一些让我们难以忘怀的人,尤其是在感情的世...
我不再什么作文600字初一通... 我不再什么作文600字初一 第一篇泥土到处都有,可当你不再闻到泥土的芬芳时,才会觉得它宝贵;植物随处...
七年级语文月考1(经典3篇) 七年级语文月考1 篇一:我眼中的好老师作为一名七年级学生,我曾经遇到过很多老师。有些老师严厉,有些老...
你快乐就好-初中作文【优质5... 你快乐就好-初中作文 篇一快乐是一种美妙的情绪,它能够让人心情愉悦、精神焕发。而我认为,一个人的快乐...
初一我收获了友谊作文700字... 初一我收获了友谊作文700字 第一篇面,风很大,天气阴沉沉的。“怦怦怦!怦怦怦!”“1、2、3、4…...
初一暑假一件事作文500字(... 初一暑假一件事作文500字 篇一初一暑假,我参加了一次短期夏令营活动。这是我第一次参加夏令营,我非常...
初中英语人称代词语法【经典3... 初中英语人称代词语法 篇一人称代词在英语语法中扮演着重要的角色。它们用来代替名词,并且根据人称的不同...
初一记忆中的暖流作文(优选6... 初一记忆中的暖流作文 篇一初一是我人生中一个重要的阶段,那段时间充满了回忆和暖流。初一的生活虽然紧张...
包装无悔生命初一作文(精选5... 包装无悔生命初一作文 篇一包装无悔生命生命是一场旅程,每个人都在这个旅程中扮演着不同的角色,承载着不...
初一满分写景作文共50篇 初一满分写景作文 第一篇时间真快,转眼间我就初一了,整整一个暑假都没有看见过母校的美景了。真是“归来...
月亮抒情作文范文初一推荐90... 月亮抒情作文范文初一 第一篇又到了一年一度的中秋节,我很高兴,因为我喜欢赏月,喜欢听中秋的美丽传说,...
青春风采初中作文(优秀5篇) 青春风采初中作文 篇一:追逐梦想的青春青春是一段美好的时光,是我们追逐梦想的时刻。初中时期,正是我们...
中秋奇趣初中作文(精简5篇) 中秋奇趣初中作文 篇一中秋佳节,是中国传统的重要节日之一。在这一天,人们会与家人团聚,品尝美食,赏月...
七年级我来了作文700字推荐... 七年级我来了作文700字 第一篇经过一个暑假的放松,我终于走进了初中校园的大门。满怀着激动与兴奋,我...