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篇)  主持词要注意活动对象,针对活动对象写相应的主持词。在现今...
新年升旗仪式致辞 新年升旗仪式致辞(精选14篇)  在现实生活或工作学习中,说到致辞,大家肯定都不陌生吧,致辞具有思路...
表演半台词 表演三句半台词  敲锣打鼓走圆场  1:锣鼓一响好心情,  2:我们漫游动画城;  3:表演一个三句...
毕业30周年同学聚会主持词 毕业30周年同学聚会主持词范文  老同学聚会,一桌饭菜,谈论着当年的同学情,好不快活呀,往日是多么的...
结训典礼主持词 结训典礼主持词范文  主持词是主持人在节目进行过程中用于串联节目的串联词。在当今中国社会,各种集会中...
集团董事长新年的经典致辞 集团董事长新年的经典致辞(通用13篇)  在平平淡淡的学习、工作、生活中,大家对致辞都不陌生吧,致辞...
郭德纲于谦相声《李菁表妹》台... 郭德纲于谦相声《李菁表妹》台词  郭德纲,1973年1月18日出生于天津,华语相声演员,电视、电影演...
教师节学生主持稿 教师节学生主持稿  教师“不仅仅是一份工作、一个职业,而是一项需要用一生的情感去拥抱的事业,更是一种...
主持词开场白夏天 主持词开场白夏天(精选5篇)  在现实社会中,很多情况下我们都需要用到开场白,独具匠心的开场白,才能...
幼儿园家长会全过程主持词 幼儿园家长会全过程主持词  主持词要把握好吸引观众、导入主题、创设情境等环节以吸引观众。在当今不断发...
播音主持岗位实践报告范文8篇 播音主持岗位实践报告范文 第一篇时间过的很快,转眼间,我到临沂银雀山汉墓竹筒博物馆工作,已经快一年的...
爱国活动主持词 爱国活动主持词范文  主持人在台上表演的灵魂就表现在主持词中。我们眼下的社会,主持词在各种活动中起到...
“我的祖国”演讲比赛主持词 “我的祖国”演讲比赛主持词  主持词要注意活动对象,针对活动对象写相应的主持词。在当下这个社会中,越...
舞蹈节目主持词串词 舞蹈节目主持词串词范文(精选8篇)  主持词需要富有情感,充满热情,才能有效地吸引到观众。在当下的社...
我是歌手歌唱比赛主持词 我是歌手歌唱比赛主持词  小歌手主持词篇一  A:尊敬的各位领导  B:敬爱的老师,亲爱的同学们  ...
中秋节联欢晚会主持词 中秋节联欢晚会主持词(精选11篇)  主持词要注意活动对象,针对活动对象写相应的主持词。我们眼下的社...
初中新生开学欢迎词 初中新生开学欢迎词2017各位初一年全体同学石狮二中敞开胸怀迎接你们,真诚地欢迎你们加入这个大家庭,...
品鉴会主持词 品鉴会主持词  借鉴诗词和散文诗是主持词的一种写作手法。随着中国在不断地进步,各种集会的节目都通过主...
新年半台词 新年三句半台词  三句半是一种中国民间群众传统曲艺表演形式。每段内容有三长句一半句。一般由4人演出,...
消夏文艺晚会的主持词 消夏文艺晚会的主持词(精选11篇)  主持词是主持人在节目进行过程中用于串联节目的串联词。在各种集会...