SpringBoot2集成Shiro與Web應用

本文結合一個簡單的權限模塊設計來實現Shiro的集成。
新建實體如下:
權限實體Permission:id,code,name,parent_id;
角色實體Role:id,code,name;
用戶實體User:id,username,password,role(簡化設計,一個用戶只能有一個角色,因此User表中設置一個role_id字段關聯角色);
Role和Permission的關係通過role_permission關係表維護。

具體見源代碼https://github.com/wu-boy/parker.git,parker-shiro-base模塊,resources目錄下有建表和初始化SQL。

SpringBoot集成Shiro引入shiro-spring-boot-web-starter即可。

首先自定義MyRealm,在這個Realm中做登錄認證和用戶授權。

package com.wu.parker.shiro.base.shiro;

import com.wu.parker.shiro.base.po.Permission;
import com.wu.parker.shiro.base.po.User;
import com.wu.parker.shiro.base.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

/**
 * @author: wusq
 * @date: 2018/12/8
 */
public class MyRealm extends AuthorizingRealm {

    private static final Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);

    @Autowired
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("授權");
        String username = (String) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = null;
        try {
            authorizationInfo = new SimpleAuthorizationInfo();
            User user = userService.findByUsername(username);
            authorizationInfo.addRole(user.getRole().getCode());
            List<Permission> list = user.getRole().getPermissionList();
            for(Permission p:list){
                authorizationInfo.addStringPermission(p.getCode());
            }
        } catch (Exception e) {
            log.error("授權錯誤{}", e.getMessage());
            e.printStackTrace();
        }
        return authorizationInfo;
    }

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

        log.info("登錄認證");

        String username = (String) token.getPrincipal();
        User user = userService.findByUsername(username);
        if(user == null) {
            throw new UnknownAccountException(); // 沒找到帳號
        }

        /*if(Boolean.TRUE.equals(user.getLocked())) {
            throw new LockedAccountException(); //帳號鎖定
        }*/

        //交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,如果覺得人家的不好可以在此判斷或自定義實現
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user.getUsername(), //用戶名
                user.getPassword(), //密碼
                getName()  //realm name
        );

        return authenticationInfo;
    }

}

ShiroConfig配置如下

package com.wu.parker.shiro.base.config;

import com.wu.parker.shiro.base.shiro.MyRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author: wusq
 * @date: 2018/12/8
 */
@Configuration
public class ShiroConfig {

    /**
     * 注入自定義的realm,告訴shiro如何獲取用戶信息來做登錄認證和授權
     */
    @Bean
    public Realm realm() {
        return new MyRealm();
    }

    /**
     * 這裏統一做鑑權,即判斷哪些請求路徑需要用戶登錄,哪些請求路徑不需要用戶登錄。
     * 這裏只做鑑權,不做權限控制,因爲權限用註解來做。
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();

        // 設置哪些請求可以匿名訪問
        chain.addPathDefinition("/login/**", "anon");

        // 由於使用Swagger調試,因此設置所有Swagger相關的請求可以匿名訪問
        chain.addPathDefinition("/swagger-ui.html", "anon");
        chain.addPathDefinition("/swagger-resources", "anon");
        chain.addPathDefinition("/swagger-resources/configuration/security", "anon");
        chain.addPathDefinition("/swagger-resources/configuration/ui", "anon");
        chain.addPathDefinition("/v2/api-docs", "anon");
        chain.addPathDefinition("/webjars/springfox-swagger-ui/**", "anon");

        //除了以上的請求外,其它請求都需要登錄
        chain.addPathDefinition("/**", "authc");
        return chain;
    }

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用於解決一個奇怪的bug。在引入spring aop的情況下。
         * 在@Controller註解的類的方法中加入@RequiresRole註解,會導致該方法無法映射請求,導致返回404。
         * 加入這項配置能解決這個bug
         */
        creator.setUsePrefix(true);
        return creator;
    }
}

新建PermissionController用來測試

package com.wu.parker.shiro.base.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: wusq
 * @date: 2018/12/8
 */
@Api(description = "資源服務")
@RestController
@RequestMapping("/security/permissions/")
public class PermissionController {

    @ApiOperation("查詢資源")
    @GetMapping()
    @RequiresPermissions("permission:retrieve")
    public String get(){
        return "有permission:retrieve這個權限的用戶才能訪問,不然訪問不了";
    }
}

新建LoginController完成登錄功能

package com.wu.parker.shiro.base.controller;

import com.wu.parker.shiro.base.po.User;
import com.wu.parker.shiro.base.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: wusq
 * @date: 2018/12/8
 */
@Api(description = "登錄服務")
@RestController
@RequestMapping("/login/")
public class LoginController {

    private static final Logger log = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private UserService userService;

    @ApiOperation("登錄")
    @GetMapping("{username}/{password}")
    public User login(@PathVariable String username, @PathVariable String password){
        User result = null;
        Subject subject = SecurityUtils.getSubject();

        // 此處的密碼應該是按照後臺的加密規則加密過的,不應該傳輸明文密碼
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try {
            subject.login(token);
            result = userService.findByUsername(username);
        } catch (UnknownAccountException e) {
            log.error("用戶名或密碼錯誤");
            e.printStackTrace();
        } catch (IncorrectCredentialsException e) {
            log.error("用戶名或密碼錯誤");
            e.printStackTrace();
        } catch (AuthenticationException e) {
            //其他錯誤,比如鎖定,如果想單獨處理請單獨catch處理
            log.error("其他錯誤");
            e.printStackTrace();
        }
        return result;
    }
}

啓動工程後,可以先訪問PermissionController中的路徑,會提示404,因爲沒有登錄,被Shiro攔截了。
再測試登錄功能,通過正確的用戶名和密碼登錄後,再訪問PermissionController會返回正常結果。

相關的注意事項都在代碼註釋中說明了。

附上加密工具類EncryptUtils,方便對Shiro的加密方式進行理解和測試
package com.wu.parker.common.encrypt;

import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * 加解密工具類
 * @author: wusq
 * @date: 2018/12/8
 */
public class EncryptUtils {

    /**
     * 默認加密次數
     */
    public static final Integer DEFAULT_ITERATIONS = 1;

    /**
     * Shiro的MD5加密,加密方式是對字符串salt+password進行加密
     * @param salt 鹽
     * @param password 密碼
     * @return
     */
    public static String shiroMd5(String salt, String password){
        String algorithmName = "MD5";
        ByteSource byteSalt = ByteSource.Util.bytes(salt);
        SimpleHash simpleHash = new SimpleHash(algorithmName, password, byteSalt, DEFAULT_ITERATIONS);
        return simpleHash.toHex();
    }

    /**
     * Java的MD5加密,加密方式是對字符串salt+password進行加密
     * @param salt 鹽
     * @param password
     * @return
     */
    public static String md5(String salt, String password){
        String result = null;
        byte[] bytes = null;
        try {
            // 生成一個MD5加密計算摘要
            MessageDigest md = MessageDigest.getInstance("MD5");
            // 對字符串進行加密
            md.update((salt + password).getBytes());
            // 獲得加密後的數據
            bytes = md.digest();

            // 將加密後的數據轉換爲16進制數字
            result = new BigInteger(1, bytes).toString(16);// 16進制數字
            // 如果生成數字未滿32位,需要前面補0
            for (int i = 0; i < 32 - result.length(); i++) {
                result = "0" + result;
            }
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("沒有md5這個算法!");
        }
        return result;
    }

    public static void main(String[] args) {

        String password1 = shiroMd5("admin", "12345678");
        System.out.println(password1);

        String password2 = md5("admin", "12345678");
        System.out.println(password2);

        // 兩者加密結果相同
        System.out.println(password1.equals(password2));
    }
}

源代碼

https://github.com/wu-boy/parker.git
parker-shiro-base模塊
EncryptUtils位於parker-common模塊

參考資料

1、跟我學Shiro
2、Shiro用starter方式優雅整合到SpringBoot中
3、springboot(十四):springboot整合shiro-登錄認證和權限管理
4、Shiro登陸異常 did not match the expected credentials. 是爲什麼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章