spring-boot-shiro-jwt-redis實現登陸授權功能


一、前言

在微服務中我們一般采用的是無狀態登錄,而傳統的session方式,在前后端分離的微服務架構下,如繼續使用則必將要解決跨域sessionId問題、集群session共享問題等等。這顯然是費力不討好的,而整合shiro,卻很不恰巧的與我們的期望有所違背:

  1. shiro默認的攔截跳轉都是跳轉url頁面,而前后端分離后,后端並無權干涉頁面跳轉。
  2. shiro默認使用的登錄攔截校驗機制恰恰就是使用的session。

這當然不是我們想要的,因此如需使用shiro,我們就需要對其進行改造,那么要如何改造呢?我們可以在整合shiro的基礎上自定義登錄校驗,繼續整合JWT,或者oauth2.0等,使其成為支持服務端無狀態登錄,即token登錄。

二、需求

  1. 首次通過post請求將用戶名與密碼到login進行登入;
  2. 登錄成功后返回token;
  3. 每次請求,客戶端需通過header將token帶回服務器做JWT Token的校驗;
  4. 服務端負責token生命周期的刷新
  5. 用戶權限的校驗;

三、實現

pom.xml

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

        <!--JWT-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.7.0</version>
        </dependency>

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

ShiroConfig

/**
 * shiro 配置類
 */
@Configuration
public class ShiroConfig {
    /**
     * Filter Chain定義說明
     * 1、一個URL可以配置多個Filter,使用逗號分隔
     * 2、當設置多個過濾器時,全部驗證通過,才視為通過
     * 3、部分過濾器可指定參數,如perms,roles
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 攔截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不會被攔截的鏈接 順序判斷
        filterChainDefinitionMap.put("/sys/login", "anon"); //登錄接口排除
        filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/**/*.js", "anon");
        filterChainDefinitionMap.put("/**/*.css", "anon");
        filterChainDefinitionMap.put("/**/*.html", "anon");
        filterChainDefinitionMap.put("/**/*.jpg", "anon");
        filterChainDefinitionMap.put("/**/*.png", "anon");
        filterChainDefinitionMap.put("/**/*.ico", "anon");

        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/user/test", "anon"); //測試

        // 添加自己的過濾器並且取名為jwt
        Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
        filterMap.put("jwt", new JwtFilter());
        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);

        return securityManager;
    }

    /**
     * 下面的代碼是添加注解支持
     *
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

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

}

ShiroRealm

/**
 * 用戶登錄鑒權和獲取用戶授權
 */
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    @Lazy
    private ISysUserService sysUserService;
    @Autowired
    @Lazy
    private RedisUtil redisUtil;

    /**
     * 必須重寫此方法,不然Shiro會報錯
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 功能: 獲取用戶權限信息,包括角色以及權限。只有當觸發檢測用戶權限時才會調用此方法,例如checkRole,checkPermission
     *
     * @param principals token
     * @return AuthorizationInfo 權限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("————權限認證 [ roles、permissions]————");
        SysUser sysUser = null;
        String username = null;
        if (principals != null) {
            sysUser = (SysUser) principals.getPrimaryPrincipal();
            username = sysUser.getUserName();
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 設置用戶擁有的角色集合,比如“admin,test”
        Set<String> roleSet = sysUserService.getUserRolesSet(username);
        info.setRoles(roleSet);

        // 設置用戶擁有的權限集合,比如“sys:role:add,sys:user:add”
        Set<String> permissionSet = sysUserService.getUserPermissionsSet(username);
        info.addStringPermissions(permissionSet);
        return info;
    }

    /**
     * 功能: 用來進行身份認證,也就是說驗證用戶輸入的賬號和密碼是否正確,獲取身份驗證信息,錯誤拋出異常
     *
     * @param auth 用戶身份信息 token
     * @return 返回封裝了用戶信息的 AuthenticationInfo 實例
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        if (token == null) {
            log.info("————————身份認證失敗——————————IP地址:  " + CommonUtils.getIpAddrByRequest(SpringContextUtils.getHttpServletRequest()));
            throw new AuthenticationException("token為空!");
        }
        // 校驗token有效性
        SysUser loginUser = this.checkUserTokenIsEffect(token);
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }

    /**
     * 校驗token的有效性
     *
     * @param token
     */
    public SysUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        // 解密獲得username,用於和數據庫進行對比
        String username = JwtUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token非法無效!");
        }

        // 查詢用戶信息
        SysUser loginUser = new SysUser();
        SysUser sysUser = sysUserService.getUserByName(username);
        if (sysUser == null) {
            throw new AuthenticationException("用戶不存在!");
        }

        // 校驗token是否超時失效 & 或者賬號密碼是否錯誤
        if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) {
            throw new AuthenticationException("Token失效請重新登錄!");
        }

        // 判斷用戶狀態
        if (!"0".equals(sysUser.getDelFlag())) {
            throw new AuthenticationException("賬號已被刪除,請聯系管理員!");
        }

        BeanUtils.copyProperties(sysUser, loginUser);
        return loginUser;
    }

    /**
     * JWTToken刷新生命周期 (解決用戶一直在線操作,提供Token失效問題)
     * 1、登錄成功后將用戶的JWT生成的Token作為k、v存儲到cache緩存里面(這時候k、v值一樣)
     * 2、當該用戶再次請求時,通過JWTFilter層層校驗之后會進入到doGetAuthenticationInfo進行身份驗證
     * 3、當該用戶這次請求JWTToken值還在生命周期內,則會通過重新PUT的方式k、v都為Token值,緩存中的token值生命周期時間重新計算(這時候k、v值一樣)
     * 4、當該用戶這次請求jwt生成的token值已經超時,但該token對應cache中的k還是存在,則表示該用戶一直在操作只是JWT的token失效了,程序會給token對應的k映射的v值重新生成JWTToken並覆蓋v值,該緩存生命周期重新計算
     * 5、當該用戶這次請求jwt在生成的token值已經超時,並在cache中不存在對應的k,則表示該用戶賬戶空閑超時,返回用戶信息已失效,請重新登錄。
     * 6、每次當返回為true情況下,都會給Response的Header中設置Authorization,該Authorization映射的v為cache對應的v值。
     * 7、注:當前端接收到Response的Header中的Authorization值會存儲起來,作為以后請求token使用
     * 參考方案:https://blog.csdn.net/qq394829044/article/details/82763936
     *
     * @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 (CommonUtils.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 / 1000);
            } else {
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
                // 設置超時時間
                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
            }
            return true;
        }
        return false;
    }

}

JwtFilter

/**
 * 鑒權登錄攔截器
 **/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    /**
     * 執行登錄認證
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            executeLogin(request, response);
            return true;
        } catch (Exception e) {
            throw new AuthenticationException("Token失效請重新登錄");
        }
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(CommonConstant.ACCESS_TOKEN);

        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;
        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"));
        // 跨域時會首先發送一個option請求,這里我們給option請求直接返回正常狀態
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

JwtToken

package cn.gathub.entity;

import org.apache.shiro.authc.AuthenticationToken;

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

JwtUtils

/**
 * JWT工具類
 **/
public class JwtUtil {

    // 過期時間30分鍾
    public static final long EXPIRE_TIME = 30 * 60 * 1000;

    /**
     * 校驗token是否正確
     *
     * @param token  密鑰
     * @param secret 用戶的密碼
     * @return 是否正確
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            // 根據密碼生成JWT效驗器
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
            // 效驗TOKEN
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 獲得token中的信息無需secret解密也能獲得
     *
     * @return token中包含的用戶名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成簽名,5min后過期
     *
     * @param username 用戶名
     * @param secret   用戶的密碼
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附帶username信息
        return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);

    }

    /**
     * 根據request中的token獲取用戶賬號
     *
     * @param request
     * @return
     * @throws Exception
     */
    public static String getUserNameByToken(HttpServletRequest request) throws Exception {
        String accessToken = request.getHeader(CommonConstant.ACCESS_TOKEN);
        String username = getUsername(accessToken);
        if (CommonUtils.isEmpty(username)) {
            throw new Exception("未獲取到用戶");
        }
        return username;
    }
}

LoginController

@RestController
@RequestMapping("/sys")
@Slf4j
public class LoginController {
    @Autowired
    private ISysUserService sysUserService;
    @Autowired
    private RedisUtil redisUtil;

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public Result<JSONObject> login(@RequestBody SysUser loginUser) throws Exception {
        Result<JSONObject> result = new Result<JSONObject>();
        String username = loginUser.getUserName();
        String password = loginUser.getPassWord();

        //1. 校驗用戶是否有效
        SysUser sysUser = sysUserService.getUserByName(username);
        result = sysUserService.checkUserIsEffective(sysUser);
        if (!result.isSuccess()) {
            return result;
        }

        //2. 校驗用戶名或密碼是否正確
        String userpassword = PasswordUtil.encrypt(username, password, sysUser.getSalt());
        String syspassword = sysUser.getPassWord();
        if (!syspassword.equals(userpassword)) {
            result.error500("用戶名或密碼錯誤");
            return result;
        }

        //用戶登錄信息
        userInfo(sysUser, result);

        return result;
    }

    /**
     * 退出登錄
     *
     * @param request
     * @param response
     * @return
     */
    @RequestMapping(value = "/logout")
    public Result<Object> logout(HttpServletRequest request, HttpServletResponse response) {
        //用戶退出邏輯
        String token = request.getHeader(CommonConstant.ACCESS_TOKEN);
        if (CommonUtils.isEmpty(token)) {
            return Result.error("退出登錄失敗!");
        }
        String username = JwtUtil.getUsername(token);
        SysUser sysUser = sysUserService.getUserByName(username);
        if (sysUser != null) {
            log.info(" 用戶名:  " + sysUser.getRealName() + ",退出成功! ");
            //清空用戶Token緩存
            redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token);
            //清空用戶權限緩存:權限Perms和角色集合
            redisUtil.del(CommonConstant.LOGIN_USER_CACHERULES_ROLE + username);
            redisUtil.del(CommonConstant.LOGIN_USER_CACHERULES_PERMISSION + username);
            return Result.ok("退出登錄成功!");
        } else {
            return Result.error("無效的token");
        }
    }

    /**
     * 用戶信息
     *
     * @param sysUser
     * @param result
     * @return
     */
    private Result<JSONObject> userInfo(SysUser sysUser, Result<JSONObject> result) {
        String syspassword = sysUser.getPassWord();
        String username = sysUser.getUserName();
        // 生成token
        String token = JwtUtil.sign(username, syspassword);
        redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
        // 設置超時時間
        redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);

        // 獲取用戶部門信息
        JSONObject obj = new JSONObject();
        obj.put("token", token);
        obj.put("userInfo", sysUser);
        result.setResult(obj);
        result.success("登錄成功");
        return result;
    }

}

四、演示

使用正確的用戶名密碼進行登陸,登陸成功后返回token
在這里插入圖片描述
使用錯誤的用戶名密碼進行登陸,登陸失敗
在這里插入圖片描述
headers中攜帶正確的token訪問接口
在這里插入圖片描述
headers中不攜帶token或者攜帶錯誤的token訪問接口
在這里插入圖片描述
無權限的用戶訪問接口
在這里插入圖片描述
無需登陸token也可以訪問的接口(在過濾器中將接口或者資源文件放開)
在這里插入圖片描述

五、github源碼地址

地址:https://github.com/it-wwh/sping-boot-shiro-jwt-redis.git


今天的更新到這里就結束了,拜拜!!!

感謝一路支持我的人,您的關注是我堅持更新的動力,有問題可以在下面評論留言或隨時與我聯系。。。。。。
QQ:850434439
微信:w850434439
EMAIL:gathub@qq.com

如果有興趣和本博客交換友鏈的話,請按照下面的格式在評論區進行評論,我會盡快添加上你的鏈接。

網站名稱:GatHub-HongHui'S Blog
網站地址:https://gathub.cn
網站描述:不習慣的事越來越多,但我仍在前進…就算步伐很小,我也在一步一步的前進。
網站Logo/頭像:頭像地址


我的微信公眾號,歡迎大家來撩!
在這里插入圖片描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM