SpringBoot整合Shiro JWT

SpringBoot整合Shiro JWT

参考:
https://www.jianshu.com/p/9b6eb3308294
https://blog.csdn.net/bicheng4769/article/details/86668209

1:概述

1.1、shiro

一个安全框架,但不只是一个安全框架。它能实现多种多样的功能。并不只是局限在web层。在国内的市场份额占比高于SpringSecurity,是使用最多的安全框架可以实现用户的认证和授权。比SpringSecurity要简单的多。
基于session进行会话管理
本文整合 JWT 后,需关闭原 shiro 的 session

1.2、JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
本文使用JWT作为无状态请求依据。

1.3、整合思路

  1. JWT作为Shiro的session,关闭原session管理
  2. 用户登录,使用JWT返回携带用户基本信息的 token
  3. 后续请求携带此token,filter中校验 token 有效性,从token中获取相应用户信息
  4. token 过期,重新登陆

2:SpringBoot + Shiro + JWT

maven依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

2.1、JWT 工具

JwtOperator

定义token的相关操作,包括 token生成、认证和解析

public interface JwtOperator {
    String token(TokenUser user);
    boolean verify(String token);
    TokenUser parse(String token);
}

JwtOperatorHandle

在 token 的 claim 中存储 用户信息的json串和类信息,用于反序列化bean

class JwtOperatorHandle implements JwtOperator {

    private static final String ENCODE_SECRET_KEY = "yyl-4012-secret-key";
    private static final String USER_VALUE_KEY = "user-value-key";
    private static final String USER_CLASS_KEY = "user-class-key";
    private SignatureAlgorithm algorithm = SignatureAlgorithm.HS256;

    private byte[] base64EncodeSecretKey;

    public JwtOperatorHandle() {
        base64EncodeSecretKey = Base64.getEncoder().encode(ENCODE_SECRET_KEY.getBytes());
    }

    public String token(TokenUser user) {

        Date now = new Date();

        String token = Jwts.builder()
                .claim(USER_VALUE_KEY, JSON.toJSONString(user))
                .claim(USER_CLASS_KEY, user.getClass())
                .setIssuedAt(now)
                .setExpiration(new Date(
                                now.getTime() + user.getExpirationMinutes() * 60 * 1000
                        )
                )
                .signWith(algorithm, base64EncodeSecretKey)
                .compact();

        return token;
    }

    public boolean verify(String token) {

        try {
            Jwts.parser().setSigningKey(base64EncodeSecretKey).parse(token);
        } catch (ExpiredJwtException e) {
            return false;
        }

        return true;
    }

    public TokenUser parse(String token) {

        Claims claims;
        try {
            claims = (Claims) Jwts.parser()
                    .setSigningKey(base64EncodeSecretKey)
                    .parse(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw new TokenExpiredException();
        }

        String userJson = claims.get(USER_VALUE_KEY, String.class);
        String clazzString = claims.get(USER_CLASS_KEY, String.class);

        Class<TokenUser> clazz;
        try {
            clazz = (Class) Class.forName(clazzString);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        TokenUser tokenUser = JSONObject.parseObject(userJson, clazz);

        return tokenUser;
    }
}

2.2、CustomRealm

Realm 作为 shiro 认证的主要处理,包括 认证 和 鉴权

public class CustomRealm extends AuthorizingRealm {

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 这里执行鉴权过程
     * 对于配置了需要鉴权url,执行此方法
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        String ticket = (String) principals.getPrimaryPrincipal();

        JwtOperator operator = JwtOperatorBuilder.build();
        ShiroUserDetail userDetail = (ShiroUserDetail) operator.parse(ticket);

        List<String> permissions = userDetail.getPermissions();

        // 获取当前用户信息
        // 判断权限
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permissions == null ? null : permissions.stream().collect(Collectors.toSet()));

        return info;
    }

    /**
     * 获取认证信息
     * 如判断用户存不存在
     * 用户密码是否正确等
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), "realmName");
    }
}

2.3、Shiro Filter

filter 包括 LoginFilter 和 JWTFilter,用于过滤 登录请求和普通请求

LoginFilter

处理登录请求,校验用户的登录信息,返回登录用户的基本信息和token

public class LoginFilter extends AccessControlFilter {

    private LoginService loginService;

    public LoginFilter(LoginService loginService) {
        this.loginService = loginService;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {

        HttpServletRequest servletRequest = (HttpServletRequest) request;

        String username = servletRequest.getParameter("username");
        String password = servletRequest.getParameter("password");

        ShiroLoginUser loginUser = new ShiroLoginUser(username, password);
        ShiroUserDetail userDetail = loginService.login(loginUser);

        JwtOperator jwtOperator = JwtOperatorBuilder.build();
        String token = jwtOperator.token(userDetail);

        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        map.put("user", userDetail);

        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json");
        response.getWriter().write(JSONObject.toJSONString(map));

        return false;
    }
}

LoginService

定义处理用户登录的逻辑,比如密码是否正常,用户的状态等

public interface LoginService {

    ShiroUserDetail login(ShiroLoginUser loginUser);

}

JwtFilter

处理普通请求,校验token有效性,从token中获取用户基本信息。
这里只在onAccessDenied方法中进行业务处理。
用户携带的token信息位于headers 的 Authorization 中

public class JwtFilter extends AccessControlFilter {

    private static final String TOKEN_HEADER = "Authorization";

    /*
     * 1. 返回true,shiro 就直接允许访问url
     * 2. 返回false,shiro 才会根据 onAccessDenied 的方法的返回值决定是否允许访问url
     * */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {

        // 这里直接返回 false ,让所有的有权限校验接口在 onAccessDenied 方法中处理
        return false;
    }

    /**
     * 返回结果为true表明登录通过
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {

        // 
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest servletRequest = (HttpServletRequest) request;

        String token = servletRequest.getHeader(TOKEN_HEADER);

        // 未携带 token 时,交给 shiro 判断 是否为放开权限接口(游客可访问接口)
        // 如果 当前接口需要权限访问,而又未携带 token ,shiro 会抛出 AuthorizationException,需处理此异常
        // 如果不返回 true ,则所有的 url 都需要携带 token 进行权限校验
        if (token == null || "".equals(token)){
            return true;
        }

        // 验证 token 的有效性
        JwtToken jwtToken = new JwtToken(token);
        try {
            // 权限认证
            getSubject(request, response).login(jwtToken);
        }catch (Exception e){
            e.printStackTrace();
            httpResponse.setStatus(401);
            httpResponse.getWriter().write("access denied");
            return false;
        }
        return true;
    }
}

ShiroProperties

shiro 相关的配置bean

@ConfigurationProperties(prefix = "system.shiro")
@Data
public class ShiroProperties {

    private String login;
    private String logout;

}

ShiroConfig

shiro的自定义配置,包括realm、filter、securityManager等的注入

@Configuration
@EnableConfigurationProperties(ShiroProperties.class)
public class ShiroConfig {

    private ShiroProperties properties;

    public ShiroConfig(ShiroProperties properties) {
        this.properties = properties;
    }

    @Bean
    public CustomRealm customRealm() {
        return new CustomRealm();
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, LoginService loginService) {

        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setLoginUrl(properties.getLogin());
        factoryBean.setSecurityManager(securityManager);

        // 注册jwt拦截器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JwtFilter());
        filterMap.put("login", new LoginFilter(loginService));
        factoryBean.setFilters(filterMap);

        // 设置 url 对应 filter
        Map<String, String> filterChainDefinitionMap = new HashMap<>();
        filterChainDefinitionMap.put("/**", "jwt");
        filterChainDefinitionMap.put("/login", "login");
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return factoryBean;

    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm());

        // 关闭 shiro 的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        securityManager.setSubjectFactory(new JwtSubjectFactory());

        return securityManager;
    }

    /**
     * 配置shiro对于权限注解的支持
     *
     * DefaultAdvisorAutoProxyCreator 实现了 BeanProcessor
     * 当 ApplicationContext读如所有的Bean配置信息后,这个类将扫描上下文,
     * 寻找所有的Advistor(一个Advisor是一个切入点和一个通知的组成),将这些 Advisor 应用到所有符合切入点的Bean中
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 使用 CGLIB 方式创建代理对象
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 配置shiro框架提供的切面类,用于创建代理对象
     * 这里为创建 权限校验的 advisor
     * 在 DefaultAdvisorAutoProxyCreator 的作用下生效
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
        return new AuthorizationAttributeSourceAdvisor();
    }

    @Bean
    @ConditionalOnMissingBean
    public LoginService loginService(){
        return loginUser -> {
            ShiroUserDetail shiroUserDetail = new ShiroUserDetail();
            shiroUserDetail.setUsername(loginUser.getUsername());
            List<String> permissions = new ArrayList<>();
            permissions.add("api:u2");
            shiroUserDetail.setPermissions(permissions);
            return shiroUserDetail;
        };
    }

    class JwtSubjectFactory extends DefaultWebSubjectFactory {
        @Override
        public Subject createSubject(SubjectContext context) {
            // 这里禁止创建session
            context.setSessionCreationEnabled(false);
            return super.createSubject(context);
        }
    }

}
  • loginService 这里只简单实现,具体可进行覆盖,给当前用户赋予 api:u2 权限

  • JwtSubjectFactory 为自定义的SubjectFactory,主要是禁止shiro创建session,因为要使用无状态的token就行认证鉴权

  • AuthorizationAttributeSourceAdvisor 和 DefaultAdvisorAutoProxyCreator 为了创建 权限认证的增强,主要处理 controller 中的 各个 MappingHandler (RequestMapping 对应 method),AuthorizationAttributeSourceAdvisor 的 matches 方法用于判断RequestMapping 对应 method是否需要增强处理,此增强包括权限认证等。

  • AuthorizationAttributeSourceAdvisor – matches

public boolean matches(Method method, Class targetClass) {
    Method m = method;

    if ( isAuthzAnnotationPresent(m) ) {
        return true;
    }

    //The 'method' parameter could be from an interface that doesn't have the annotation.
    //Check to see if the implementation has it.
    if ( targetClass != null) {
        try {
            m = targetClass.getMethod(m.getName(), m.getParameterTypes());
            return isAuthzAnnotationPresent(m) || isAuthzAnnotationPresent(targetClass);
        } catch (NoSuchMethodException ignored) {
            //default return value is false.  If we can't find the method, then obviously
            //there is no annotation, so just use the default return value.
        }
    }

    return false;
}
  • AuthorizationAttributeSourceAdvisor – isAuthzAnnotationPresent
private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
        new Class[] {
                RequiresPermissions.class, RequiresRoles.class,
                RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
        };
        
private boolean isAuthzAnnotationPresent(Method method) {
    for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
        Annotation a = AnnotationUtils.findAnnotation(method, annClass);
        if ( a != null ) {
            return true;
        }
    }
    return false;
}

异常处理

@ControllerAdvice
public class ShiroExceptionHandler {

    @ExceptionHandler(AuthorizationException.class)
    @ResponseBody
    public String notAuthorized(HttpServletResponse response){

        response.setStatus(401);
        return "你没有权限";

    }

    @ExceptionHandler(TokenExpiredException.class)
    @ResponseBody
    public String tokenExpired(HttpServletResponse response){

        response.setStatus(405);
        return "token 已失效,请重新登陆";

    }

}

3:测试

3.1、登录

  • url 配置为 /login在这里插入图片描述

3.1、普通接口访问

Controller定义

@GetMapping("/api/u1")
@ResponseBody
public String u1() {
    return "1";
}

@RequiresPermissions("api:u2")
@GetMapping("/api/u2")
@ResponseBody
public String u2() {
    return "2";
}

@RequiresPermissions("api:u3")
@GetMapping("/api/u3")
@ResponseBody
public String u3() {
    return "3";
}
  • /api/u1 非鉴权接口,游客可访问
    不携带token

    此时未携带 token 信息,可正常访问

    携带token
    在这里插入图片描述

    此时携带 token 信息,可正常访问

  • /api/u2 具有api:u2权限的用户可访问(在默认实现的LoginService中已给用户赋权,有权限访问)

    不携带token

    此时未携带 token 信息,/api/u2 返回 401 没有权限访问

    携带token
    在这里插入图片描述

    此时携带 token 信息,token拥有 api:u2 也就是当前接口权限,正常访问

  • /api/u3 具有api:u2权限的用户可访问,默认登录用户无权限访问

    不携带token

    此时未携带 token 信息,/api/u2 返回 401 没有权限访问

    携带token
    在这里插入图片描述

    此时携带 token 信息,token未拥有 api:u3 也就是当前接口权限,没有权限访问

发布了1 篇原创文章 · 获赞 3 · 访问量 112

猜你喜欢

转载自blog.csdn.net/qq_38245668/article/details/105775845