spring-boot-shiro-jwt-redis實現登錄受權功能

1、前言

在微服務中咱們通常採用的是無狀態登陸,而傳統的session方式,在先後端分離的微服務架構下,如繼續使用則必將要解決跨域sessionId問題、集羣session共享問題等等。這顯然是費力不討好的,而整合shiro,卻很不恰巧的與咱們的指望有所違背:css

  1. shiro默認的攔截跳轉都是跳轉url頁面,而先後端分離後,後端並沒有權干涉頁面跳轉。
  2. shiro默認使用的登陸攔截校驗機制偏偏就是使用的session。

這固然不是咱們想要的,所以如需使用shiro,咱們就須要對其進行改造,那麼要如何改造呢?咱們能夠在整合shiro的基礎上自定義登陸校驗,繼續整合JWT,或者oauth2.0等,使其成爲支持服務端無狀態登陸,即token登陸。html

2、需求

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

3、實現

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定義說明
     * 一、一個URL能夠配置多個Filter,使用逗號分隔
     * 二、當設置多個過濾器時,所有驗證經過,才視爲經過
     * 三、部分過濾器可指定參數,如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失效問題)
     * 一、登陸成功後將用戶的JWT生成的Token做爲k、v存儲到cache緩存裏面(這時候k、v值同樣)
     * 二、當該用戶再次請求時,經過JWTFilter層層校驗以後會進入到doGetAuthenticationInfo進行身份驗證
     * 三、當該用戶此次請求JWTToken值還在生命週期內,則會經過從新PUT的方式k、v都爲Token值,緩存中的token值生命週期時間從新計算(這時候k、v值同樣)
     * 四、當該用戶此次請求jwt生成的token值已經超時,但該token對應cache中的k仍是存在,則表示該用戶一直在操做只是JWT的token失效了,程序會給token對應的k映射的v值從新生成JWTToken並覆蓋v值,該緩存生命週期從新計算
     * 五、當該用戶此次請求jwt在生成的token值已經超時,並在cache中不存在對應的k,則表示該用戶帳戶空閒超時,返回用戶信息已失效,請從新登陸。
     * 六、每次當返回爲true狀況下,都會給Response的Header中設置Authorization,該Authorization映射的v爲cache對應的v值。
     * 七、注:當前端接收到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;
    }

}

4、演示

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

5、github源碼地址

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


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

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

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

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


個人微信公衆號,歡迎你們來撩! 在這裏插入圖片描述數據庫

相關文章
相關標籤/搜索