shiro+JWT做一個(gè)簡(jiǎn)單的權(quán)限管理功能

私聊我做畢設(shè)或者實(shí)驗(yàn)課題。

之前在網(wǎng)上查找一些關(guān)于shiro整合jwt的案例悔常,都比較繁瑣影斑,不適合新手學(xué)習(xí),自己把網(wǎng)上的案例學(xué)習(xí)一遍机打,做了一個(gè)類似矫户,也更加方便了解的權(quán)限管理功能。

數(shù)據(jù)庫(kù)設(shè)計(jì)残邀,網(wǎng)上一些案例都是設(shè)計(jì)好幾個(gè)表關(guān)聯(lián)皆辽,不容易理解,這里我就設(shè)計(jì)了一個(gè)表芥挣,膳汪,如果用戶角色是admin,后面會(huì)設(shè)計(jì)只有admin角色才能訪問的方法九秀,pression設(shè)計(jì)vip權(quán)限和normal權(quán)限,后面也會(huì)設(shè)計(jì)帶有vip權(quán)限或者normal權(quán)限才能訪問的方法粘我。

user

實(shí)驗(yàn)步驟

1.導(dǎo)入依賴

<dependency>
   <!-- shiro-->
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>

2.相關(guān)類的設(shè)計(jì)

shiroconfig,shirio的相關(guān)配置文件鼓蜒,都是固定的模板痹换,我們主要學(xué)習(xí)ShiroFilterFactoryBean 處理攔截資源文件問題,在里面我們添加自己的過濾器都弹,所有的請(qǐng)求都會(huì)經(jīng)過自定義的過濾器進(jìn)行過濾

@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(MyRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(realm);

        /**
         * 關(guān)閉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);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 添加自己的過濾器并且取名為jwt
        Map<String, Filter> filterMap = new HashMap<>(4);
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);

        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/401");

        /**
         * 自定義url規(guī)則
         */
        Map<String, String> filterRuleMap = new HashMap<>(4);
        // 所有請(qǐng)求通過我們自己的JWT Filter

        filterRuleMap.put("/**", "jwt");
        // 訪問401和404頁(yè)面不通過我們的Filter
        filterRuleMap.put("/401", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * 下面的代碼是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 強(qiáng)制使用cglib,防止重復(fù)代理和可能引起代理出錯(cuò)的問題
        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;
    }
}

JWTFilter畅厢,自定義的過濾器,當(dāng)發(fā)送過來(lái)的請(qǐng)求有攜帶token信息冯痢,就會(huì)執(zhí)行executeLogin方法,進(jìn)入到自定義的Realm進(jìn)行授權(quán)和認(rèn)證的功能框杜,因?yàn)槭且胨说哪0迤珠梗杂行┑胤讲恍枰梢宰⑨尩簦瑢?duì)跨域的支持暫時(shí)用不到咪辱。這里我在每一個(gè)方法后面都添加一個(gè)輸出語(yǔ)句是為了方便對(duì)程序運(yùn)行過程的理解振劳。

public class JWTFilter extends BasicHttpAuthenticationFilter {
    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    /**
     * 判斷用戶是否想要登入。
     * true:是要登錄
     *
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        System.out.println("*****進(jìn)入isLoginAttempt");
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("token");
        return authorization != null;
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response)  throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        //得到token信息
        String authorization = httpServletRequest.getHeader("token");

        JWTToken token = new JWTToken(authorization);
        // 提交給realm進(jìn)行登入油狂,如果錯(cuò)誤他會(huì)拋出異常并被捕獲
        getSubject(request, response).login(token);
        // 如果沒有拋出異常則代表登入成功历恐,返回true
        return true;
    }

    /**
     * 這里我們?cè)敿?xì)說明下為什么最終返回的都是true,即允許訪問
     * 例如我們提供一個(gè)地址 GET /article
     * 登入用戶和游客看到的內(nèi)容是不同的
     * 如果在這里返回了false专筷,請(qǐng)求會(huì)被直接攔截弱贼,用戶看不到任何東西
     * 所以我們?cè)谶@里返回true,Controller中可以通過 subject.isAuthenticated() 來(lái)判斷用戶是否登入
     * 如果有些資源只有登入用戶才能訪問磷蛹,我們只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是這樣做有一個(gè)缺點(diǎn)吮旅,就是不能夠?qū)ET,POST等請(qǐng)求進(jìn)行分別過濾鑒權(quán)(因?yàn)槲覀冎貙懥斯俜降姆椒?,但實(shí)際上對(duì)應(yīng)用影響不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                System.out.println("執(zhí)行executeLogin");
                executeLogin(request, response);
            } catch (Exception e) {
                response401(request, response);
            }
        }
        System.out.println("沒有執(zhí)行executeLogin");
        return true;
    }

//    /**
//     * 對(duì)跨域提供支持
//     */
//    @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"));
//        // 跨域時(shí)會(huì)首先發(fā)送一個(gè)option請(qǐng)求弦聂,這里我們給option請(qǐng)求直接返回正常狀態(tài)
//        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
//            httpServletResponse.setStatus(HttpStatus.OK.value());
//            return false;
//        }
//        return super.preHandle(request, response);
//    }

    /**
     * 將非法請(qǐng)求跳轉(zhuǎn)到 /401
     */
    private void response401(ServletRequest req, ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/401");
        } catch (IOException e) {
            LOGGER.error(e.getMessage());
        }
    }
}

當(dāng)發(fā)送過來(lái)的請(qǐng)求攜帶token信息鸟辅,就會(huì)進(jìn)入到自定義Realm類中進(jìn)行認(rèn)證,繼承AuthorizingRealm 類并重寫里面的認(rèn)證和授權(quán)的方法。首先觀察doGetAuthenticationInfo方法莺葫,這里我們首先獲得token信息匪凉,然后通過JwtUtils工具類對(duì)token信息進(jìn)行解析,獲得當(dāng)前用戶的姓名捺檬,然后去數(shù)據(jù)庫(kù)中查到當(dāng)前用戶再层,如果當(dāng)前用戶存在,則把當(dāng)前用戶信息保存到 SimpleAuthenticationInfo對(duì)象中堡纬,后面做授權(quán)要用到(第一個(gè)參數(shù)用來(lái)保存當(dāng)前用戶信息聂受,也可以保存token信息,不唯一烤镐,自己定義)

@Configuration
public class MyRealm extends AuthorizingRealm {
   @Autowired
   private UserService userServicel;
    /*
     必須要加蛋济,不然程序會(huì)報(bào)錯(cuò)
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }
    /*
    這里的PrincipalCollection對(duì)應(yīng)SimpleAuthenticationInfo的第一個(gè)參數(shù)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        System.out.println("執(zhí)行————————》AuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//        User user=(User)principal.getPrimaryPrincipal();
//        System.out.println(user);
        String username = JwtUtils.getUsername(principal.toString());
        System.out.println(username);
        User user = userServicel.getOne(new QueryWrapper<User>().eq("username", username));
        info.addRole(user.getRole());
        info.addStringPermission(user.getPression());
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        System.out.println("執(zhí)行————————》AuthenticationInfo");
        String token = (String) auth.getCredentials();
        System.out.println("token信息"+token);
        // 解密獲得username,用于和數(shù)據(jù)庫(kù)進(jìn)行對(duì)比
        String username = JwtUtils.getUsername(token);
        System.out.println(username);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }
        User user = userServicel.getOne(new QueryWrapper<User>().eq("username", username));
        if (user == null) {
            throw new AuthenticationException("User didn't existed!");
        }

        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }
}
public class JWTToken implements AuthenticationToken {

    /**
     * 密鑰
     */
    private String token;

    public JWTToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}
public class JwtUtils {
    //定義兩個(gè)常量炮叶,1.設(shè)置過期時(shí)間 2.密鑰(隨機(jī)碗旅,由公司生成)
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
    //生成token字符串渡处,用戶id和名稱(可以寫多個(gè))
    public static String getJwtToken(String username, String password){

        String JwtToken = Jwts.builder()
                //設(shè)置token的頭信息
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                //設(shè)置過期時(shí)間
                .setSubject("user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                //設(shè)置token的主題部分
                .claim("username", username)
                .claim("password", password)
                //簽名哈希
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /**
     * 判斷token是否存在與有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
            //驗(yàn)證是否有效的token
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
    /**
     * 根據(jù)token信息得到username
     * @param jwtToken
     * @return
     */
    public static String getUsername(String jwtToken) {
            //驗(yàn)證是否有效的token
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            //得到字符串的主題部分
            Claims claims = claimsJws.getBody();
            return (String)claims.get("username");
    }
    /**
     * 判斷token是否存在與有效
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根據(jù)token獲取會(huì)員id
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        //得到字符串的主題部分
        Claims claims = claimsJws.getBody();
        return (String)claims.get("username");
    }
}

這里我們可以對(duì)認(rèn)證功能進(jìn)行試驗(yàn)。使用postman測(cè)試祟辟,創(chuàng)建一個(gè)loginController類医瘫,得到當(dāng)前用戶的token信息。

  @PostMapping("/login")
    public R login(@RequestBody JSONObject requestJson){
        System.out.println(requestJson);
        String username = requestJson.getString("username");
        String password = requestJson.getString("password");
        String token = JwtUtils.getJwtToken(username, password);
        return R.ok().data("token",token);
    }
image.png

我們隨便寫一個(gè)請(qǐng)求旧困,用戶測(cè)試

 @GetMapping("user")
    //@RequiresRoles("admin")
    //@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
    public R getUser(){
        return R.ok();
    }

沒有攜帶token信息

image.png

查看輸出臺(tái)

image.png

攜帶token信息

image.png

查看輸出臺(tái)醇份,執(zhí)行了AuthenticationInfo方法

image.png

現(xiàn)在我們就可以開始做授權(quán)認(rèn)證的功能,觀察doGetAuthorizationInfo方法吼具,這里的PrincipalCollection對(duì)應(yīng)SimpleAuthenticationInfo的第一個(gè)參數(shù)僚纷,通過principal獲取當(dāng)前用戶,并授予權(quán)限馍悟,使用addRole添加用戶角色畔濒,使用 addStringPermission添加用戶的權(quán)限,還需要了解2個(gè)注解锣咒,@RequiresRoles 授權(quán)方法中給用戶添加角色侵状,@RequiresPermissions 授權(quán)方法中給用戶添加權(quán)限。

 @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        System.out.println("執(zhí)行————————》AuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//        User user=(User)principal.getPrimaryPrincipal();
//        System.out.println(user);
        String username = JwtUtils.getUsername(principal.toString());
        System.out.println(username);
        User user = userServicel.getOne(new QueryWrapper<User>().eq("username", username));
        info.addRole(user.getRole());
        info.addStringPermission(user.getPression());
        return info;
    }

我們?cè)谥暗恼?qǐng)求方法中添加@RequiresRoles注解毅整,可以從控制臺(tái)中看到執(zhí)行了授權(quán)的方法趣兄,但是因?yàn)楫?dāng)前用戶的角色是guest,所以會(huì)報(bào)錯(cuò)悼嫉。

 @GetMapping("user")
    @RequiresRoles("admin")
    //@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
    public R getUser(){
        return R.ok();
    }
image.png

image.png

修改用戶的角色為guest,再發(fā)送請(qǐng)求

  @GetMapping("user")
    @RequiresRoles("guest")
    //@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
    public R getUser(){
        return R.ok();
    }

訪問成功

image.png

用戶權(quán)限的操作跟用戶角色操作類似艇潭,只不過注解不一樣,可以自己試試戏蔑,對(duì)于shiro的學(xué)習(xí)只是一個(gè)簡(jiǎn)單的理解蹋凝,如有不足望提出。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末总棵,一起剝皮案震驚了整個(gè)濱河市鳍寂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌情龄,老刑警劉巖迄汛,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異骤视,居然都是意外死亡鞍爱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門专酗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)睹逃,“玉大人,你說我怎么就攤上這事祷肯∥簦” “怎么了粱玲?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拜轨。 經(jīng)常有香客問我,道長(zhǎng)允青,這世上最難降的妖魔是什么橄碾? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮颠锉,結(jié)果婚禮上法牲,老公的妹妹穿的比我還像新娘。我一直安慰自己琼掠,他們只是感情好拒垃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瓷蛙,像睡著了一般悼瓮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上艰猬,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天横堡,我揣著相機(jī)與錄音,去河邊找鬼冠桃。 笑死命贴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的食听。 我是一名探鬼主播胸蛛,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼樱报!你這毒婦竟也來(lái)了葬项?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤肃弟,失蹤者是張志新(化名)和其女友劉穎玷室,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笤受,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡穷缤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了箩兽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片津肛。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖汗贫,靈堂內(nèi)的尸體忽然破棺而出身坐,到底是詐尸還是另有隱情秸脱,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布部蛇,位于F島的核電站摊唇,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏涯鲁。R本人自食惡果不足惜巷查,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望抹腿。 院中可真熱鬧岛请,春花似錦、人聲如沸警绩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肩祥。三九已至后室,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搭幻,已是汗流浹背咧擂。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留檀蹋,地道東北人松申。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像俯逾,于是被迫代替她去往敵國(guó)和親贸桶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354