Spring Security基于jwt实现权限校验

一 引言

基于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;
    }

    
}

定义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 {
    
    
    @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类中

猜你喜欢

转载自blog.csdn.net/Instanceztt/article/details/128132940