1. 程式人生 > >Shiro結合JWT實現單點登入

Shiro結合JWT實現單點登入

簡述

Apache Shiro是java的一個安全框架,Shiro可以幫助我們完成認證、授權、加密、會話管理、與Web整合、快取等。而且Shiro的API也比較簡單,這裡我們就不進行過多的贅述,想要詳細瞭解Shiro的,推薦看開濤的部落格(點這裡

在Shiro的強大許可權管理的基礎上,我們實現單點登入就容易了很多,結合我上篇部落格所講的JSON Web Token(推薦先看這篇部落格)就可以完成單點登入系統。

實現過程

在使用Shiro實現登入的時候,將登入成功的資訊包括Token資訊返回給前端,前端在請求後臺時,將Token資訊存入請求頭中。配置自定義攔截器,攔截所有URL請求,取出請求頭資訊中的Token資訊,對Token資訊進行驗證,對於redis中存在的登入時生成的Token資訊,如果Token資訊正確,則確認該使用者已經登入,否則拒絕請求,返回401錯誤。

1.引入所需jar包

        <!--json-web-token-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
        </dependency>

2.登入認證

要實現單點登入功能,首先要完成的就是登入功能,這裡我們使用Shiro的認證來完成登入。

2.1 spring-shiro的配置檔案

<!--配置SecurityManager-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realms">
            <list>
                <ref bean="shiroRealm"/>
            </list>
        </property>
    </bean>

    <!--配置Realm-->
    <!--直接配置實現了org.apache.shiro.realm.Realm介面的bean-->
    <bean id="shiroRealm" class="com.why.authority.realms.ShiroRealm">
        <!-- 憑證匹配器:配置登入驗證所使用的加密演算法-->
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="sha-512"/>
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
    </bean>

    <!-- shiro攔截器-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/user/index"/>
        <!--配置哪些頁面需要受保護
            1. anon 可以被匿名訪問
            2. authc 必須認證(登入)後才可能訪問的頁面
            3. logout 登出
        -->

        <!--自定義filters,該攔截器即為實現單點登入的攔截器-->
        <property name="filters">
            <map>
                <entry key="acf">
                    <bean class="com.why.authority.filter.AccessingControlFilter"/>
                </entry>
            </map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /user/index=anon
                /user/login=anon
                /user/content= acf
                /** = acf
            </value>
        </property>
    </bean>

2.2 登入方法

Controller:

@RequestMapping(value = {"/login"}, method = RequestMethod.POST)
    @ResponseBody
    public WhyResult content(@RequestParam("usercode") String usercode, @RequestParam("password") String password) {

        String userInfoKey = "aum:user:" + usercode;
        String tokenKey = "aum:token:"+usercode;

        try {
            if(StringUtils.isBlank(usercode) || StringUtils.isBlank(password)){
                throw new UnknownAccountException();
            }
            //1. 執行登入
            //把使用者名稱和密碼封裝為UsernamePasswordToken物件
            UsernamePasswordToken token = new UsernamePasswordToken(usercode, password);
            SecurityUtils.getSubject().login(token);

            //2.獲取使用者資訊userEntity,redis中不存在則存入redis
            UserEntity userEntity = new UserEntity();
            //2.1 從redis中獲取或從資料庫中獲取
            String strUserInfo = JedisCacheUtil.get(userInfoKey);
            if (!StringUtils.isBlank(strUserInfo)) {
                userEntity = JacksonJsonUntil.jsonToPojo(strUserInfo, UserEntity.class);
            } else {
                userEntity = addUserInfoToRedis(usercode, userInfoKey);
            }
            //3.生成Token資訊並儲存到redis
            LoginEntity loginEntity = addTokenToRedis(userEntity,tokenKey);
            return WhyResult.build(200,"登入成功!",loginEntity);
            //所有認證異常的父類
        } catch (AuthenticationException e) {
            logger.error("登入失敗!",e);
            return WhyResult.build(401,"使用者名稱或密碼錯誤!");
        }
    }

自定義Realm

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //1.把AuthenticationToken轉換為UsernamePasswordToken
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;

        //2.從UsernamePasswordToken中獲取userCode
        String userCode = usernamePasswordToken.getUsername();
        String userInfoKey = "aum:user:" + userCode;
        UserEntity userEntity;
        //3.獲取使用者資訊userEntity
        //3.1 從redis中獲取
        String strUserInfo;
        try {
            strUserInfo = JedisCacheUtil.get(userInfoKey);
            if (!StringUtils.isBlank(strUserInfo)) {
                userEntity = JacksonJsonUntil.jsonToPojo(strUserInfo, UserEntity.class);
            } else {
                userEntity = addUserAndGetUser(userCode, userInfoKey);
            }
        } catch (Exception e) {
            userEntity = addUserAndGetUser(userCode, userInfoKey);
        }
        //6.根據使用者的情況,來構建AuthenticationInfo物件並返回
        String credentials = userEntity.getPassword();
        //使用ByteSource.Util.bytes()來計算鹽值
        ByteSource credentialsSalt = ByteSource.Util.bytes(userCode);

        return new SimpleAuthenticationInfo(userEntity, credentials, credentialsSalt, getName());

    }

3. 自定義攔截器

該攔截器是在spring-shiro.xml檔案中配置的自定義攔截器,原理就是攔截每個請求,驗證URL請求頭資訊中的Token資訊是否過期,是否被篡改。

@Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //是否驗證通過
        boolean bool = false;
        try {
            HttpServletRequest req = WebUtils.toHttp(servletRequest);
            String firstLoginToken = req.getParameter("token");

            //從token中獲得資訊
            Claims claims = TokenUtil.getClaims(firstLoginToken);
            String userCode = claims.getSubject();
            String userId = claims.getId();

            String redisLoginKey = "aum:token:" + userCode;
            String redisToken = JedisCacheUtil.get(redisLoginKey);
            if(!StringUtils.isBlank(redisToken)){
                String[] arrayRedisToken = redisToken.split("@");
                //將使用者傳過來的token和redis中的做對比,若一樣,認為已經登入
                if (arrayRedisToken[0].equals(firstLoginToken)) {
                    //比較這次訪問與登入的時間間隔有多少分鐘,如果大於5分鐘,則更新redis中的上次訪問時間資訊,將過期時間從新設定為30分鐘
                    long diffMin = TokenUtil.CompareTime(arrayRedisToken[1]);
                    if (diffMin >= 5) {
                        String currentAccessTime = PasswordUtil.base64Encoede(String.valueOf(System.currentTimeMillis()));
                        //更新redis中的token登入資訊
                        JedisCacheUtil.set(redisLoginKey, arrayRedisToken[0] + "@" + currentAccessTime, 30 * 60);
                    }
                    bool=true;
                }
            }
        } catch (Exception e) {
            return bool;
        }
        return bool;
    }

至此為止,關鍵程式碼已經展示完了,現在實現的僅僅是最基礎的單點登入,還需要進行更多的安全檢查和驗證,這裡就不介紹了。

--------------------- 本文來自 王洪玉 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/why15732625998/article/details/78647375?utm_source=copy