Shiro+Jwt总结

1 篇文章 0 订阅

shiro是什么

用于进行加密,认证授权的java安全框架
核心对象:
Subject:用户对象,封装信息
SecurityManager:管理所有用户
Realm:连接数据,进行用户认证和授权

Jwt是什么

JWT全称JSON Web Token,是javaweb官方提供的一种封装用户数据,保证权限访问的字符串。
详情见:https://blog.csdn.net/m0_51433562/article/details/119609637?spm=1001.2014.3001.5502

原理讲解

知道了Shiro和Jwt是什么玩意儿之后,那么接下来我们要探究如何使用Shiro和Jwt完成用户认证和授权的呢?

认证(Authentication)和授权(Authorization)

首先我们先弄懂认证和授权到底有什么区别,平常我们都是将认证授权两个词语连在一起进行讨论,实际上认证授权是两个完全不同的概念。

举个很简单的例子,我们去网吧上网的时候,网管首先会让你出示你的身份证件,核实你的身份。确定你是成年人之后,网管会激活你的号码或者会员,这样你就可以在网吧内入座了。

在上述的例子中,网管核实你是否为成年人就是一个认证的过程。认证通过之后,网管激活号码,你获得在网吧内就做的权利,这就是一个授权的过程。相信通过这个案例,大家对于认证和授权应该有了理解。

Shiro的认证和授权

那么在shiro安全框架中又是怎样实现认证和授权的呢?
在javaWeb开发中,我们会遇到这样的场景,某些页面或者接口是只有特定的用户可以进行访问和调用的,比如VIP视频只允许VIP用户观看,后台管理接口只允许管理员登录。此时shiro就能很好的帮助我们完成这个需求。

原理图
分析上图:

  1. 客户端发送携带JwtToken的请求到后端,会根据Sercurity Manager配置的拦截器进行判断,进入不同的Filter过滤器中(以JwtFilter为例)。
  2. Filter过滤器会对请求进行拦截,获取请求携带的Token。在过滤器中创建subject对象,这相当与一个用户对象,利用subject对象并携带从客户端获取的token,调用Sercurity Manager,将subject对象统一交给Sercurity Manager去处理。(注:原生shiro中subject对象会对传递来的username和password进行封装形成token,因为我们采用的是jwt,传来的就是token,所以不必封装)
  3. subject对象进入Sercurity Manager后,会将携带的Token交给JwtRealm的认证授权程序进行处理,认证通过后,放可执行controller中的请求,否则会抛出异常。

代码编写

了解了shiro和JWT是如何共同实现认证和授权之后,就可以很轻松的完成代码的编写。先介绍我们要用到的类

  • ShiroCofig:Shiro的配置类,用于配置subject,securityManager和realm。
  • JwtRealm:自定义的Realm对象,用于连接数据,进行认证和授权。
  • JwtToken:自定义的token类,用以代替shiro原生的UsernamePasswordToken
  • JwtDefaultSubjectFactory(可选):自定义的subjectFactory,继承于DefaultSubjectFactory,用于生产subject对象。
  • JwtFilter:需要进行jwt认证的API接口经过的过滤器。
  • CommonFilter:不需要进行jwt认证的API接口经过的过滤器。
  • JwtUtils:JWT的工具类,用于decode,encode和识别token(具体代码在前面提到的博客中有)

ShiroConfig

ShiroConfig用于进行Shiro的相关配置,主要包括ShiroFilterFactoryBean、DefaultWebSecurityManager和Realm的配置。

  • ShiroFilterFactoryBean:用来生产subject,配置过滤器和拦截路由。
  • DefaultWebSecurityManager:获得SecurityManager,对subject进行统一管理。还可以进行Session Dao和Session Manager等的配置(相关配置,本文章省略)
  • Realm:放回自定义的Realm对象
@Configuration
public class ShiroConfig {
    //三大核心对象:Subject、SecurityManager、Realm

    //告诉shiro不创建内置的session
    @Bean
    public SubjectFactory subjectFactory(){
        return new JwtDefaultSubjectFactory();
    }

    //3、ShiroFilterFactoryBean->Subject subject是用户主题,进入到securitymanager中
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //关联securityManager
        bean.setSecurityManager(getDefaultWebSecurityManager());
        //添加内置过滤器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("anon",new CommonFilter());
        filterMap.put("jwt",new JwtFilter());
        bean.setFilters(filterMap);
        //添加拦截器,对路由进行限制
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        /*
            anon:无需认证可以直接访问
            auth:必须认证才能访问
            user:必须拥有 记住我功能才能用
            perms:拥有对某个资源的权限才能访问
            role:拥有某个角色权限才能访问
         */
        filterRuleMap.put("/treasure/user/login","anon");
        filterRuleMap.put("/treasure/user/getUserInfoByEmail/**","jwt");
        bean.setFilterChainDefinitionMap(filterRuleMap);
        return bean;
    }

    //2、DefaultWebSecurityManager->SecurityManager 管理所有用户,利用realm完成数据连接
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联realm
        securityManager.setRealm(realm());
        return securityManager;
    }

    //1、Realm->Realm 需要自定义一个Realm用于存储数据,这里使用jwtRealm
    @Bean
    public Realm realm() {
        return new JwtRealm();
    }
}

JwtRealm

自定义的Realm对象,该对象继承于AuthorizingRealm,实现了Shiro具体认证和授权的方法。doGetAuthenticationInfo用于授权,doGetAuthorizationInfo用于认证。
另外,重写了supports方法,表示该Realm只用于识别JwtToken
JwtRealm最终会配置为ShiroConfig中Realm的返回对象,并于SecurityManager进行关联。

@Component
public class JwtRealm extends AuthorizingRealm {

    @Autowired
    private JwtUtils jwtUtils;

    /*
     * 多重写一个support
     * 标识这个Realm是专门用来验证JwtToken
     * 不负责验证其他的token(UsernamePasswordToken)
     * */
    @Override
    public boolean supports(AuthenticationToken token) {
        //这个token就是从过滤器中传入的jwtToken
        return token instanceof JwtToken;
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String jwt = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRole("user");
        return authorizationInfo;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String jwt = (String) authenticationToken.getPrincipal();
        if(!jwtUtils.isVerify(jwt)){
            throw new IncorrectCredentialsException("Authorization token is invalid");
        }
        return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
    }
}

JwtToken

JwtToken是定义的一个Token类,继承了AuthenticationToken类,实现getPrincipal和getCredentials方法,(这两个方法本来是用于获取token中的信息,和识别token的,但JwtUtils已经为我们提供了这样的方法,所以这两个方法对于jwtToken没有意义)。用于将客户端传来的token进行封装,便于Realm识别token类型,进行认证和授权。

/*
    JwtToken代替原生的UsernamePasswordToken
 */
public class JwtToken implements AuthenticationToken {

    private String jwt;

    public JwtToken(String jwt) {
        this.jwt = jwt;
    }

    //返回原来的字符串,解析交给JwtUtils实现
    @Override
    public Object getPrincipal() {
        return jwt;
    }

    //返回原来的字符串,解析交给JwtUtils实现
    @Override
    public Object getCredentials() {
        return jwt;
    }
}

JwtDefaultSubjectFactory

继承于DefaultSubjectFactory类,重写了生产工厂,关闭Shiro的session存储

public class JwtDefaultSubjectFactory extends DefaultSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        //不创建shiro内部的session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

JwtFilter

JwtFilter定义为需要进行jwt认证的API结果经过的过滤器,在这个过滤器中,取出API请求中包含的JWT字符串,封装为jwtToken对象,同时创建一个subject,调用login方法进行认证和授权。JwtFilter继承于AccessControlFilter类,实现isAccessAllowed和onAccessDenied方法。

//需要认证的url经过该过滤器
public class JwtFilter extends AccessControlFilter {

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        httpResponse.setHeader("Access-Control-Allow-Origin", httpRequest.getHeader("Origin"));
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Content-Type","application/json;charset=UTF-8");
        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpResponse.setHeader("Access-Control-Allow-Methods", httpRequest.getMethod());
            httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
            return true;
        }
        return super.preHandle(request, response);
    }

    /*
     * 1. 返回true,shiro就直接允许访问url
     * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
     * */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        String jwt = ((HttpServletRequest) servletRequest).getHeader("Authorization");
        if(jwt != null){
            JwtToken jwtToken = new JwtToken(jwt);
            //这里getSubject方法实际上就是获得一个subject
            //与原生shiro不同的地方在于没有对username和password进行封装
            //直接使用jwt进行认真,login方法实际上就是交给Realm进行认证
            try{
                getSubject(servletRequest,servletResponse).login(jwtToken);
            }catch (Exception e){
                e.printStackTrace();
                return false;
            }
            return true;
        }
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
        //直接设置401 未认证
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return false;
    }
}

CommonFilter

CommonFilter是不需要进行认证的API接口经过的过滤器,只需要进行跨域处理

// 不需要进行认证的url经过该过滤器
public class CommonFilter extends AnonymousFilter {

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        httpResponse.setHeader("Access-Control-Allow-Origin", httpRequest.getHeader("Origin"));
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Content-Type","application/json;charset=UTF-8");
        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpResponse.setHeader("Access-Control-Allow-Methods", httpRequest.getMethod());
            httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
            return true;
        }
        return super.preHandle(request, response);
    }
}

写了这么多类,大家可能有点懵,实际上仔细看看代码里面的注释还是很容易理解的。为了更清晰的描述之间的关系,见下图。

shiro调用图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值