shiro

1、什么是权限管理

权限管理包括用户身份认证和授权两部分,简称认证授权。

身份认证:就是判断一个用户是否为合法用户的处理过程。最简单的就是通过核对用户名和密码,看是否与系统中存储的该用户的用户名和密码一致,来判断身份是否正确

授权:即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限可访问的资源。

2、整体架构

在这里插入图片描述

SecurityManager 外部应用与subject进行交互,subject记录当前操作对象,subject在shiro中是个接口,接口中定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权

Authenticator 做认证

Authorizer 做授权

Session Manager 存储用户session

Session DAO 对用户Session的操作

Cache Manager 缓存用户认证和授权的信息

Pluggable Realms 获取认证和授权信息并判断,可以操作JDBC,NoSql数据库等等,说白了认证和授权器都用的他

3、shiro中认证的关键对象

subject:主体 其实就是登录的用户或程序

principal:身份信息 大多数情况下是用户名,是主体进行身份认证的标识,标识必须唯一

credential:凭证信息 是只有主体自己知道的安全信息,如密码,证书等。通常情况下是密码。

4、认证授权

主体(登录用户)登录网站,输入用户名和密码,对应shiro中的身份信息principal和凭证creditial ,将这两个玩意封装为token(令牌),shiro再进行认证(认证器和授权器调用Realms来判断信息是否符合)
流程图
认证授权
在这里插入图片描述
用户登录大致流程
在这里插入图片描述
权限五张表关系图
在这里插入图片描述

5、小demo

public static void main(String[] args) {
        //创建安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();

        //给安全管理器设置Realms  做数据调配
        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));

        //securityUtils工具类  给全局安全工具类设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);

        //subject主体
        Subject subject = SecurityUtils.getSubject();

        //创建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("xiaocheng","123");

        //认证
        try{
            System.out.println("认证状态"+subject.isAuthenticated());
            subject.login(token);
            System.out.println("认证状态"+subject.isAuthenticated());

        }catch(UnknownAccountException e){
            System.out.println("账号不正确");
        }catch(IncorrectCredentialsException e ){
            System.out.println("密码不正确");
        }
}

6、源码分析

DelegatingSubject类
public void login(AuthenticationToken token) throws AuthenticationException {
        this.clearRunAsIdentitiesInternal();
        Subject subject = this.securityManager.login(this, token);  //往里进执行认证操作
        String host = null;
        PrincipalCollection principals;
        if (subject instanceof DelegatingSubject) { //执行到这里,表示认证成功,并返回一个代表登录用户的subject
            DelegatingSubject delegating = (DelegatingSubject)subject;
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }

        if (principals != null && !principals.isEmpty()) {
            this.principals = principals;  //认证成功,拿到用户名
            this.authenticated = true;  //设置认证字段为true
            if (token instanceof HostAuthenticationToken) {
                host = ((HostAuthenticationToken)token).getHost();
            }

            if (host != null) {
                this.host = host;
            }

            Session session = subject.getSession(false);  //拿到subject主体的session对象
            if (session != null) {
                this.session = this.decorate(session);    //将认证信息保存至session中
            } else {
                this.session = null;
            }

        } else {
            String msg = "Principals returned from securityManager.login( token ) returned a null or empty value.  This value must be non null and populated with one or more elements.";
            throw new IllegalStateException(msg);
        }
    }



DefaultSecutiryManager类
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = this.authenticate(token);   //往里进执行认证操作
        } catch (AuthenticationException var7) {
            AuthenticationException ae = var7;

            try {
                this.onFailedLogin(token, ae, subject);
            } catch (Exception var6) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an exception.  Logging and propagating original AuthenticationException.", var6);
                }
            }

            throw var7;
        }

        Subject loggedIn = this.createSubject(token, info, subject);//回溯回来后,新建subject对象,用于表示登录用户
        this.onSuccessfulLogin(token, info, loggedIn);
        return loggedIn;  //返回subject主体,继续回溯
    }



AuthenticatingSecutityManager类
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);  //经过回溯回来的用户名被封装为AuthenticationInfo类
    }



AbstractAuthenticator类
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        if (token == null) {
            throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
        } else {
            log.trace("Authentication attempt received for token [{}]", token);

            AuthenticationInfo info;
            try {
                info = this.doAuthenticate(token);  //往里进执行认证操作
                if (info == null) {
                    String msg = "No account information found for authentication token [" + token + "] by this Authenticator instance.  Please check that it is configured correctly.";
                    throw new AuthenticationException(msg);
                }
            } catch (Throwable var8) {
                AuthenticationException ae = null;
                if (var8 instanceof AuthenticationException) {
                    ae = (AuthenticationException)var8;
                }

                if (ae == null) {
                    String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected error? (Typical or expected login exceptions should extend from AuthenticationException).";
                    ae = new AuthenticationException(msg, var8);
                    if (log.isWarnEnabled()) {
                        log.warn(msg, var8);
                    }
                }

                try {
                    this.notifyFailure(token, ae);
                } catch (Throwable var7) {
                    if (log.isWarnEnabled()) {
                        String msg = "Unable to send notification for failed authentication attempt - listener error?.  Please check your AuthenticationListener implementation(s).  Logging sending exception and propagating original AuthenticationException instead...";
                        log.warn(msg, var7);
                    }
                }

                throw ae;
            }

            log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);
            this.notifySuccess(token, info);  //用户名和密码都认证成功,设置token成功标志
            return info;  //继续返回用户名
        }
    }



ModularRealmAuthenticator类
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        this.assertRealmsConfigured();
        Collection<Realm> realms = this.getRealms();
        return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);  //往里进执行认证操作
    }
    
    
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        if (!realm.supports(token)) {
            String msg = "Realm [" + realm + "] does not support authentication token [" + token + "].  Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts AuthenticationTokens of this type.";
            throw new UnsupportedTokenException(msg);
        } else {
            AuthenticationInfo info = realm.getAuthenticationInfo(token);  //往里进执行认证操作
            if (info == null) {
                String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "].";
                throw new UnknownAccountException(msg);
            } else {
                return info;  //返回认证成功的用户名 ,继续回溯
            }
        }
    }



AuthenticatingRealm类
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
        if (info == null) {
            info = this.doGetAuthenticationInfo(token);  //往里进执行认证操作
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {  
                this.cacheAuthenticationInfoIfPossible(token, info);  //将用户名缓存,往下执行判断密码凭证
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }

        if (info != null) {  //info为用户名认证成功返回的用户名
            this.assertCredentialsMatch(token, info);  //认证密码凭证,里边用的是equals,大致是两个数组元素分别比较
        } else {
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
        }

        return info;  //到此,密码和用户名都认证成功了,返回用户名,继续回溯
    }



SimpleAccountRealm类
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken)token;  
        SimpleAccount account = this.getUser(upToken.getUsername());  //往里进执行用户名认证
        if (account != null) {
            if (account.isLocked()) {  //判断账号是否被锁
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }

            if (account.isCredentialsExpired()) {  //判断账号的密码是否过期
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }
        }

        return account;  //回溯上去,到此用户名认证成功
    }

总结:

1、首先进行的是用户名principal的认证,认证成功后,再进行密码credential的认证,都认证成功了,才生成登录用户对应得subject对象存储在session中

2、进行用户名认证是在SimpleAccountRealm类中的doGetAuthenticationInfo方法中。进行密码凭证认证的是在

AuthenticatingRealm类中的getAuthenticationInfo方法中

3、根据继承关系,Realm接口–》CacheingRealm实现类–》AuthenticatingRealm实现类–》AuthorizingRealm实现类–》SimpleAccountRealm实现类,认证用户名的方法doGetAuthenticationInfo是定义在AuthenticatingRealm类中,所以,我们可以通过继承AuthenticatingRealm或AuthorizingRealm来重写doGetAuthenticationInfo方法,而密码凭证认证是直接在AuthenticatingRealm类中实现的。其实,shiro将密码由它自己管理也是有道理的:密码很重要,不应该由程序员来操作,同时,涉及到加盐加密时,shiro可以自己操作。

4、我们可以继承AuthenticatingRealm或AuthorizingRealm类,重写doGetAuthenticationInfo方法来自定义认证用户名,在里面调用数据库啊,redis啥的,自定义认证。

5、对了,CacheingRealm类是用来做缓存的,AuthenticatingRealm类是用来做认证的,AuthorizingRealm是用来做授权的,所以,如果你既想自定义认证,又想自定义授权,那就只能继承AuthorizingRealm类

6、SimpleAuthorizationInfo类用来存储subject主体的授权信息,SimpleAuthenticationInfo类用来存储subject主体的认证信息

7、自定义realm

public class CustomerRealm extends AuthorizingRealm {
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //从token中获取用户名
        String principle  = (String)authenticationToken.getPrincipal();
        System.out.println("在自定义中得到的用户名为:"+principle);
        //根据身份信息去MySQL或redis查询判断
        if(principle.equals("xiaocheng")){
            return new SimpleAuthenticationInfo("xiaocheng","123","CustomerRealm");
        }
        return null;
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
}

public class testCustomerRealm {
    public static void main(String[] args) {
        //创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

        //设置自定义realm
        defaultSecurityManager.setRealm(new CustomerRealm());

        //将安全工具类设置安全工具类
        SecurityUtils.setSecurityManager(defaultSecurityManager);

        //通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();

        //创建token
        UsernamePasswordToken token = new UsernamePasswordToken("xiaocheng","123");

        //认证
        try{
            subject.login(token);
            System.out.println(subject.isPermitted());
        }catch(Exception e){
            e.printStackTrace();
        }

    }
}

8、盐

加盐加密,密文存储

MD5介绍

作用:一般用来加密或签名

特点:MD5是非对称加密算法,不可逆,只能由明文转换为密文,无论相同内容执行多少次md5生成的结果始终是一致的(可以理解为幂等性操作)

生成结果:始终是一个16进制的32位长度字符串

对于加盐加密的讨论:

首先,加盐是必须的,如果不加盐,用户注册的账号和密码在数据库中以明文存储,如果被黑,就完蛋。

通过随机生成的盐结合账号,再使用MD5进行加密后得到的密码在数据库中存储,则是密文形式了。

这时有个问题啊,如果用户登录呢?怎么判断用户登录的账号密码和数据库中查到的账号密码是一致的呢?当初随机生成的盐我们不知道啊。

所以,为了解决这个问题,需要在设计数据库时,需设计盐字段来保存随机生成的盐。

这时候,可能有人又会问:我知道数据库中的账号密码和盐了,那不是还可以拿到账号密码吗?

注意,shiro只是尽可能的保证数据安全,不是百分百,而且,这种方式下,数据库中存储的是密文密码和盐,黑客还是比较难攻克的。

demo,用户登录使用哈希加盐加散列和数据库比较

public class testCustomerMD5 {
    public static void main(String[] args) {
        //创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

        //注入realm
        CustomerMD5Realm realm = new CustomerMD5Realm();

        //设置realm使用hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //使用的算法
        credentialsMatcher.setHashAlgorithmName("md5");
        //散列的次数
        credentialsMatcher.setHashIterations(23);
        realm.setCredentialsMatcher(credentialsMatcher);


        //设置自定义realm
        defaultSecurityManager.setRealm(realm);



        //将安全工具类设置安全工具类
        SecurityUtils.setSecurityManager(defaultSecurityManager);

        //通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();

        //创建token
        UsernamePasswordToken token = new UsernamePasswordToken("xiaocheng","123");

        //认证
        try{
            subject.login(token);
            System.out.println(subject.isAuthenticated());
            System.out.println("认证成功");
        }catch(UnknownAccountException e){
            System.out.println("账号错误");
        }catch(IncorrectCredentialsException e ){
            System.out.println("密码错误");
        }
    }
    
public class CustomerMD5Realm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String principal = (String)authenticationToken.getPrincipal();
        if(principal.equals("xiaocheng")){
            return new SimpleAuthenticationInfo("xiaocheng","ca62283396fccb5eae5f78f9f1fce6eb", ByteSource.Util.bytes("x*0y"),"CustomerMd5Realm");
        }
        return null;
    }
}

总结:shiro默认使用的是CredentialsMatcher,凭证匹配器,这完意默认使用的是equals方法。所以,如果我们想要登录时将账号密码加盐加密和数据库中查找到的比较,需要使用这玩意的子类HashedCredentialsMatcher

然后主要操作的就是CredentialsMatcher。

9、授权

授权可以理解为who对what进行how操作

who可以理解为登录的用户(主体subject),what理解为资源(可以是页面,按钮等等),how理解为权限(增删改查)

认证授权口述:

用户通过网页登录,携带身份信息(用户名)和凭证信息(密码),进入后台,shiro将其转为token进行认证,如果合法,继续进行授权

授权方式:

基于角色的访问控制

if(subject.hasRole("admin")){
    //满足则继续
}

基于资源的访问控制

if(subject.isPermission("user:*create"))

权限字符串

规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作

例如:

用户创建权限:user:create或user:create:*

用户修改实例001的权限:user:update:001

shiro中授权编程实现方式

编程式

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")){
    //有权限
}else {
    //无权限
}

注解式

@RequiresRoles("admin")
public void hello(){
    //有权限
}

标签式

JSP/GSP 标签:在JSP/GSP页面通过相应的标签完成
<shiro:hasRole name = "admin">
<!-有权限->
</shiro:hasRole>
注意:Thymeleaf 中使用shiro需要额外集成

总结:shiro的授权可以通过前端或后端来设置

10、整合spring boot

整体思路:前端请求被shiro框架中的shiroFilter拦截,shiroFilter会调用securityManager管理器进行认证,安全管理器势必要调用数据进行校验,继而用到了Realm操作数据库或redis等拿取数据,认证完成后如果是受限资源,还需要进行授权操作,如果是公共资源,则直接可以使用。

shiroFilter–>SecurityManager–>Realm–>credentialsMatcher(凭证匹配器,算法,hash次数)

shiro常见的过滤器

配置缩写 对应过滤器 功能
anon AnonymousFilter 指定url可以匿名访问
authc FormAuthenticationFilter 指定url需要form表单登录

11、权限信息的缓存

用户的权限信息基本上不会有太大的变化,我们不可能每次用户登录都要查询数据库进行认证授权操作,频繁的访问数据库会导致数据库压力大。

所以,我们一般会给权限信息加缓存

1、使用CacheManager

基本流程:用户登录–》shiro拦截–》如果此用户的授权信息已在缓存管理器

CacheManager中,则直接读取,如果没有,则去数据库中查询然后放入缓存,待下次授权查询时,直接找缓存

shiro默认使用EhCache实现缓存(了解即可)

使用步骤:引入依赖–》Realm开启缓存管理

前面说过继承关系:Realm接口–》CacheingRealm实现类–》AuthenticatingRealm实现类–》AuthorizingRealm实现类–》SimpleAccountRealm实现类

在CacheingRealm中的CacheManager管理器,我们可以修改它。实现自定义缓存管理。

这种缓存是本地缓存,当本地服务宕机,则缓存消失。我们需要做成分布式缓存,这样,即使某个应用程序死机,其他应用程序任然可以使用缓存,这涉及到redis。

2、redis作为缓存

public class RedisCacheManager implements CacheManager{
    
}public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        this.logger.debug("get cache, name=" + name);
        Cache cache = (Cache)this.caches.get(name);
        if (cache == null) {
            cache = new RedisCache(this.redisManager, this.keySerializer, this.valueSerializer, this.keyPrefix + name + ":", this.expire, this.principalIdFieldName);
            this.caches.put(name, cache);
        }

        return (Cache)cache;
    }

相当于 Map<String,Map<String,String>>

12、jeecgboot框架中对shiro的实现

/**
 * @Description: 鉴权登录拦截器
 * @Author: Scott
 * @Date: 2018/10/7
 **/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    private boolean allowOrigin = true;  //表示允许token跨域

    public JwtFilter(){}
    public JwtFilter(boolean allowOrigin){
        this.allowOrigin = allowOrigin;
    }

    /**
     * 执行登录认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            executeLogin(request, response);  //验证token
            return true;
        } catch (Exception e) {
            throw new AuthenticationException("Token失效,请重新登录", e);
        }
    }

    /**
     *此方法验证登录的token
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        //token存储在请求头,拿到token
        String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
        // update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
        if(token == null){
            token = httpServletRequest.getParameter("token");
        }
        // update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
      //把登录中的token转为JwtToken
        JwtToken jwtToken = new JwtToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        if(allowOrigin){
            httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
            //update-begin-author:scott date:20200907 for:issues/I1TAAP 前后端分离,shiro过滤器配置引起的跨域问题
            // 是否允许发送Cookie,默认Cookie不包括在CORS请求之中。设为true时,表示服务器允许Cookie包含在请求中。
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
            //update-end-author:scott date:20200907 for:issues/I1TAAP 前后端分离,shiro过滤器配置引起的跨域问题
        }
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        //update-begin-author:taoyan date:20200708 for:多租户用到
        String tenant_id = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
        TenantContext.setTenant(tenant_id);
        //update-end-author:taoyan date:20200708 for:多租户用到
        return super.preHandle(request, response);
    }
}

ResourceCheckFilter类主要用于路径的拦截,用于权限,表示用户访问某个url是否允许

/**
 * @Author Scott
 * @create 2019-02-01 15:56
 * @desc 鉴权请求URL访问权限拦截器
 */
@Slf4j
public class ResourceCheckFilter extends AccessControlFilter {

    private String errorUrl;

    public String getErrorUrl() {
        return errorUrl;
    }

    public void setErrorUrl(String errorUrl) {
        this.errorUrl = errorUrl;
    }

    /**
     * 表示是否允许访问 ,如果允许访问返回true,否则false;
     *
     * @param servletRequest
     * @param servletResponse
     * @param o               表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
     * @return
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        //通过请求对象获取subject主体
        Subject subject = getSubject(servletRequest, servletResponse);
        String url = getPathWithinApplication(servletRequest);
        log.info("当前用户正在访问的 url => " + url);
        return subject.isPermitted(url);
    }

    /**
     * onAccessDenied:表示当访问拒绝时是否已经处理了; 如果返回 true 表示需要继续处理; 如果返回 false
     * 表示该拦截器实例已经处理了,将直接返回即可。
     *
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        log.info("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.sendRedirect(request.getContextPath() + this.errorUrl);

        // 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
        return false;
    }

}

JwtToken类,自定义token对象

public class JwtToken implements AuthenticationToken {
	
	private static final long serialVersionUID = 1L;
	private String token;
 
    public JwtToken(String token) {
        this.token = token;
    }
 
    @Override
    public Object getPrincipal() {
        return token;
    }
 
    @Override
    public Object getCredentials() {
        return token;
    }
}

继承的AuthenticationToken,这个类是shiro中的认证信息token。

ShiroConfig类,这个类主要是用于配置shiro,包括:shiroFilter,securityManager、cacheManager(redis实现)

@Slf4j
@Configuration
public class ShiroConfig {

    @Value("${jeecg.shiro.excludeUrls}")
    private String excludeUrls;  //从类路径下的配置文件中读取排除的Url
    @Resource
    LettuceConnectionFactory lettuceConnectionFactory;
    @Autowired
    private Environment env;


    /**
     * Filter Chain定义说明
     *
     * 1、一个URL可以配置多个Filter,使用逗号分隔
     * 2、当设置多个过滤器时,全部验证通过,才视为通过
     * 3、部分过滤器可指定参数,如perms,roles
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        if(oConvertUtils.isNotEmpty(excludeUrls)){
            String[] permissionUrl = excludeUrls.split(",");
            for(String url : permissionUrl){
                filterChainDefinitionMap.put(url,"anon");
            }
        }
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
       

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
        //如果cloudServer为空 则说明是单体 需要加载跨域配置
        Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
        filterMap.put("jwt", new JwtFilter(cloudServer==null));
        shiroFilterFactoryBean.setFilters(filterMap);
        // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
        filterChainDefinitionMap.put("/**", "jwt");

        // 未授权界面返回JSON
        shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
        shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-
         * StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        //自定义缓存实现,使用redis
        securityManager.setCacheManager(redisCacheManager());
        return securityManager;
    }

    /**
     * 下面的代码是添加注解支持
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        /**
         * 解决重复代理问题 github#994
         * 添加前缀判断 不匹配 任何Advisor
         */
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisCacheManager redisCacheManager() {
        log.info("===============(1)创建缓存管理器RedisCacheManager");
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        //redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
        redisCacheManager.setPrincipalIdFieldName("id");
        //用户权限信息缓存时间
        redisCacheManager.setExpire(200000);
        return redisCacheManager;
    }

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    @Bean
    public IRedisManager redisManager() {
        log.info("===============(2)创建RedisManager,连接Redis..");
        IRedisManager manager;
        // redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
        if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
            RedisManager redisManager = new RedisManager();
            redisManager.setHost(lettuceConnectionFactory.getHostName());
            redisManager.setPort(lettuceConnectionFactory.getPort());
            redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
            redisManager.setTimeout(0);
            if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
                redisManager.setPassword(lettuceConnectionFactory.getPassword());
            }
            manager = redisManager;
        }else{
            // redis集群支持,优先使用集群配置
            RedisClusterManager redisManager = new RedisClusterManager();
            Set<HostAndPort> portSet = new HashSet<>();
            lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
            //update-begin--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
            if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
                JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
                    lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
                redisManager.setPassword(lettuceConnectionFactory.getPassword());
                redisManager.setJedisCluster(jedisCluster);
            } else {
                JedisCluster jedisCluster = new JedisCluster(portSet);
                redisManager.setJedisCluster(jedisCluster);
            }
            //update-end--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
            manager = redisManager;
        }
        return manager;
    }

}

RedisCacheManager类,自定义redis缓存类

 public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        this.logger.debug("get cache, name=" + name);
        Cache cache = (Cache)this.caches.get(name);
        if (cache == null) {
            cache = new RedisCache(this.redisManager, this.keySerializer, this.valueSerializer, this.keyPrefix + name + ":", this.expire, this.principalIdFieldName);
            this.caches.put(name, cache);
        }

        return (Cache)cache;
    }

这个name是shiro的缓存管理器全限定名称,相当于一个大key,这个大key保存了map集合,map集合中保存了这个缓存管理器对应的用户的k,v形式的权限信息。

如果没有缓存,则创建缓存并设置过时时间。

ShiroRealm类,是用于提供数据供其认证和授权操作。

/**
 * @Description: 用户登录鉴权和获取用户授权
 */
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
	@Lazy
    @Resource
    private CommonAPI commonAPI;  //公用模块

    @Lazy
    @Resource
    private RedisUtil redisUtil;  //redis工具类

    /**
     * 必须重写此方法,不然Shiro会报错,表示在shiro的Realm层面的token必须为自定义的JWTToken格式
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
     * 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
     *
     * @param principals 身份信息
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("===============Shiro权限认证开始============ [ roles、permissions]==========");
        String username = null;
        if (principals != null) {
            //得到用户的主身份,唯一标识
            LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
            username = sysUser.getUsername();
        }
        //定义授权信息对象
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 设置用户拥有的角色集合,比如“admin,test”
        Set<String> roleSet = commonAPI.queryUserRoles(username);
        System.out.println(roleSet.toString());
        info.setRoles(roleSet);

        // 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
        Set<String> permissionSet = commonAPI.queryUserAuths(username);
        info.addStringPermissions(permissionSet);
        System.out.println(permissionSet);
        log.info("===============Shiro权限认证成功==============");
        return info;
    }

    /**
     * 用户信息认证是在用户进行登录的时候进行验证(不存redis)
     * 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
     *
     * @param auth 用户登录的账号密码信息
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
        String token = (String) auth.getCredentials();
        if (token == null) {
            log.info("————————身份认证失败——————————IP地址:  "+ oConvertUtils.getIpAddrByRequest(SpringContextUtils.getHttpServletRequest()));
            throw new AuthenticationException("token为空!");
        }
        // 校验token有效性
        LoginUser loginUser = this.checkUserTokenIsEffect(token);
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }

    /**
     * 校验token的有效性
     *
     * @param token
     */
    public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        // 解密获得username,用于和数据库进行对比
        String username = JwtUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token非法无效!");
        }

        // 查询用户信息
        log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
        LoginUser loginUser = commonAPI.getUserByName(username);
        if (loginUser == null) {
            throw new AuthenticationException("用户不存在!");
        }
        // 判断用户状态
        if (loginUser.getStatus() != 1) {
            throw new AuthenticationException("账号已被锁定,请联系管理员!");
        }
        // 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
            throw new AuthenticationException("Token失效,请重新登录!");
        }

        return loginUser;
    }

    /**
     * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
     * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
     * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
     * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
     *       用户过期时间 = Jwt有效时间 * 2。
     *
     * @param userName
     * @param passWord
     * @return
     */
    public boolean jwtTokenRefresh(String token, String userName, String passWord) {
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
        if (oConvertUtils.isNotEmpty(cacheToken)) {
            // 校验token有效性
            if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                String newAuthorization = JwtUtil.sign(userName, passWord);
                // 设置超时时间
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
                log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
            }
            return true;
        }
        return false;
    }

    /**
     * 清除当前用户的权限认证缓存
     *
     * @param principals 权限信息
     */
    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }
}

流程图

认证过程
在这里插入图片描述
授权过程
在这里插入图片描述

总结:

1、用户登录–》从请求头中获取token–》将token转换为自定义JwtToken对象–》得到登录用户的subject对象,调用login,将JwtToken作为参数,进行认证。这个login如果没有自定义,则使用的shiro默认的login方法

2、如果登录成功,用户访问其他资源url,则走的是ResourceCheckFilter过滤器,这个过滤器和自定义的ShiroFilter中配置的路径权限一起使用,大致流程是:通过请求对象获取用户的subject对象,调用isPermitted方法,将url参数传递进去,如果用户的权限允许访问这个url,则返回true。

3、关于shiroRealm中的逻辑:首先,Realm层面的token必须是自定义的JwtToken ,否则 报错。

自定义授权流程(doGetAuthorizationInfo方法):得到用户的主身份,通过主身份得到用户名–》定义授权信息对象SimpleAuthorizationInfo–》通过用户名找到该用户所有的角色存入授权信息对象–》再通过用户名找到该用户所有的权限存入授权信息对象–》返回这个授权信息对象

自定义认证流程(doGetAuthenticationInfo方法):从token中得到用户的密码–》检测这个密码的有效性–》返回SimpleAuthenticationInfo认证信息对象。

检查密码的有效性流程:通过token工具类JwtUtil类从token中获取用户名,通过这个用户名找到该用户的登录细信息存放于LoginUser对象中,判断用户状态是否被锁定–》校验token是否超时失效或用户名密码是否则正确

4、对于用户认证信息,不存redis,用户的授权信息存redis。这很好理解:每次登录,认证通常检查的是用户名和密码,如果缓存,那么下一次用户登录就会出现问题。
5、JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能):
1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
5、注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
* 用户过期时间 = Jwt有效时间 * 2。

13、jeecgboot中的单点登录,结合shiro和JWT

源码分析:用户的登录和登出


```java
@ApiOperation("登录接口")
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	public Result<JSONObject> login(@RequestBody SysLoginModel sysLoginModel) {
		Result<JSONObject> res;
		Result<JSONObject> result = new Result<JSONObject>();
		String username = sysLoginModel.getUsername();
		String password = sysLoginModel.getPassword();
		//update-begin--Author:scott  Date:20190805 for:暂时注释掉密码加密逻辑,有点问题
		//前端密码加密,后端进行密码解密
		//password = AesEncryptUtil.desEncrypt(sysLoginModel.getPassword().replaceAll("%2B", "\\+")).trim();//密码解密
		//update-begin--Author:scott  Date:20190805 for:暂时注释掉密码加密逻辑,有点问题

		//update-begin-author:taoyan date:20190828 for:校验验证码
		String captcha = sysLoginModel.getCaptcha();
		if (captcha == null) {
			result.error500("验证码无效");
			res = result;
		} else {
			String lowerCaseCaptcha = captcha.toLowerCase();
			String realKey = MD5Util.MD5Encode(lowerCaseCaptcha + sysLoginModel.getCheckKey(), "utf-8");
			Object checkCode = redisUtil.get(realKey);//当进入登录页时,有一定几率出现验证码错误 #1714
			if (checkCode == null || !checkCode.toString().equals(lowerCaseCaptcha)) {
				result.error500("验证码错误");
				res = result;
			} else {//update-end-author:taoyan date:20190828 for:校验验证码
//1. 校验用户是否有效
//update-begin-author:wangshuai date:20200601 for: 登录代码验证用户是否注销bug,if条件永远为false
				LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
				queryWrapper.eq(SysUser::getUsername, username);
				SysUser sysUser = sysUserService.getOne(queryWrapper);//update-end-author:wangshuai date:20200601 for: 登录代码验证用户是否注销bug,if条件永远为false
				result = sysUserService.checkUserIsEffective(sysUser);
				if (!result.isSuccess()) {
					res = result;
				} else {//2. 校验用户名或密码是否正确
					String userpassword = PasswordUtil.encrypt(username, password, sysUser.getSalt());
					String syspassword = sysUser.getPassword();
					if (!syspassword.equals(userpassword)) {
						result.error500("用户名或密码错误");
						res = result;
					} else {//用户登录信息
						userInfo(sysUser, result);//update-begin--Author:liusq  Date:20210126  for:登录成功,删除redis中的验证码
						redisUtil.del(realKey);//update-begin--Author:liusq  Date:20210126  for:登录成功,删除redis中的验证码
						LoginUser loginUser = new LoginUser();
						BeanUtils.copyProperties(sysUser, loginUser);
						baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null, loginUser);//update-end--Author:wangshuai  Date:20200714  for:登录日志没有记录人员
						res = result;
					}
				}
			}
		}

		return res;
	}
	
	/**
	 * 退出登录
	 * @param request
	 * @param response
	 * @return
	 */
	@RequestMapping(value = "/logout")
	public Result<Object> logout(HttpServletRequest request,HttpServletResponse response) {
		//用户退出逻辑
	    String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
	    if(oConvertUtils.isEmpty(token)) {
	    	return Result.error("退出登录失败!");
	    }
	    String username = JwtUtil.getUsername(token);
		LoginUser sysUser = sysBaseAPI.getUserByName(username);
	    if(sysUser!=null) {
			//update-begin--Author:wangshuai  Date:20200714  for:登出日志没有记录人员
			baseCommonService.addLog("用户名: "+sysUser.getRealname()+",退出成功!", CommonConstant.LOG_TYPE_1, null,sysUser);
			//update-end--Author:wangshuai  Date:20200714  for:登出日志没有记录人员
	    	log.info(" 用户名:  "+sysUser.getRealname()+",退出成功! ");
	    	//清空用户登录Token缓存
	    	redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token);
	    	//清空用户登录Shiro权限缓存
			redisUtil.del(CommonConstant.PREFIX_USER_SHIRO_CACHE + sysUser.getId());
			//清空用户的缓存信息(包括部门信息),例如sys:cache:user::<username>
			redisUtil.del(String.format("%s::%s", CacheConstant.SYS_USERS_CACHE, sysUser.getUsername()));
			//调用shiro的logout
			SecurityUtils.getSubject().logout();
	    	return Result.ok("退出登录成功!");
	    }else {
	    	return Result.error("Token无效!");
	    }
	}

总结:
登录流程:得到用户名和密码和验证码–》得到验证码的低位通过MD5加密后得到一个key–》通过这个key去redis中找到对应的value(整个过程是验证验证码)–》验证用户是否有效,checkUserIsEffective方法,上面讲过,涉及到token的超时,更新和签名等操作,以保证token的有效性–》校验用户名或密码是否正确(通过比较数据库中的密码和登录用户的密码equals)–》如果前面成功,则登录成功–》通过redisUtils删除验证码缓存。

登出流程:从请求头中得到token–》通过token获取用户名–》通过用户名查询用户的登录信息–》如果前面成功,则退出成功–》清空用户登录Token缓存–》清空用户登录Shiro权限缓存—》清空用户的缓存信息(包括部门信息)

单点登录流程图
在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值