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类中

相关内容

热门资讯

掌心里的爱作文【优秀5篇】 掌心里的爱作文 篇一掌心里的爱爱是一种无私的情感,它无所不在,无处不在。在我们的生活中,有很多形式的...
全世界化成一滴蓝色的眼泪 初... 全世界化成一滴蓝色的眼泪 初中生作文 篇一全世界化成一滴蓝色的眼泪蓝色的眼泪从天空中滴落,汇聚成一滴...
我印象最深的人七年级写老师的... 我印象最深的人七年级写老师的作文 篇一我印象最深的人是我的语文老师,她是我七年级的班主任。她是一个非...
我属于你初一作文(推荐5篇) 我属于你初一作文 篇一我属于你初一作文初中生活,是我人生中重要的一个阶段。刚踏入初中的时候,我有些紧...
绿荫下的光斑初一作文(优秀3... 绿荫下的光斑初一作文 篇一绿荫下的光斑初一作文初一的暑假,我和家人来到了一个风景如画的小镇度假,这里...
初一想象作文【优选6篇】 初一想象作文 篇一翱翔的翅膀我有一双神奇的翅膀,它们是我独一无二的特殊能力。当我激动或兴奋的时候,这...
初一开学作文(最新6篇) 初一开学作文 篇一我的初一开学心情初一开学,对于我来说是一次特别的经历。我迫不及待地等待着这一天的到...
我是钻石初一作文【经典5篇】 我是钻石初一作文 篇一钻石初一是我人生中的一个重要转折点。回想起以前的日子,我觉得自己就像一个粗糙的...
初一写景作文250字通用63... 初一写景作文250字 第一篇什么是和谐?和谐是春天的第一缕阳光,第一片绿叶,第一滴雨滴,最清新的空气...
难忘的敏特英语学习初中作文(... 难忘的敏特英语学习初中作文 篇一初中时期,我曾经参加过一次难忘的敏特英语学习活动。这次活动不仅让我提...
初一开学第一篇作文(精彩3篇... 初一开学第一篇作文 篇一:新的起点开学第一天,我怀着激动的心情来到了新的学校。这是我人生中的一个新的...
我是中学生了感觉真棒作文(经... 我是中学生了感觉真棒作文 篇一终于,我升入了中学,成为了一名中学生。这是我人生中的重要转折点,我感到...
我的札记本作文(精简5篇) 我的札记本作文 篇一我的札记本是我生活中的得力助手。它陪伴我度过了许多美好的时光,记录了许多珍贵的回...
鸡年春节歌曲:迎春花歌词(精... 鸡年春节歌曲:迎春花歌词 篇一《迎春花》是一首充满喜庆和欢乐气氛的鸡年春节歌曲。这首歌曲以迎春花为主...
少年的模样-记叙文【精彩5篇... 少年的模样-记叙文 篇一夏日的阳光透过窗帘洒在地板上,照亮了少年的脸庞。他身穿一件白色的T恤,牛仔裤...
初中英语作文:蘑菇 Mush... 初中英语作文:蘑菇 Mushrooms 篇一Mushrooms are a type of fung...
遇见作文【通用6篇】 遇见作文 篇一近年来,作文成为了学生们备受关注的一项重要考试内容。然而,对于很多学生来说,作文却是一...
游藏龙百瀑初一作文【优选6篇... 游藏龙百瀑初一作文 篇一游藏龙百瀑初一作文 篇一游藏龙是我国著名的风景名胜区之一,位于贵州省黔东南苗...
初中的军训作文600字(精选... 初中的军训作文600字 篇一初中的军训是一次难忘的经历初中的军训是每个初中生都会经历的一段时光。我记...
我的未来我做主初一作文(精选... 我的未来我做主初一作文 篇一我的未来我做主未来,是一个充满无限可能的词汇。在这个时代,我们都有自己的...