SpringBoot:Spring Boot整合Shiro安全框架

最近在由Spring Boot2.x構建的更簡潔的後臺管理系統,完美整合SpringMvc + Shiro + MybatisPlus + Beetl技術,項目開發完成會開源出來,希望能對大家學習道路上有所幫助。在這一篇中我將把我整合Shiro過程記錄下來,希望對大家的學習這塊能有所幫助。

maven依賴包

<!-- shiro框架 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

        <!--shiro依賴和緩存-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
            <version>1.4.0</version>
        </dependency>

Shiro 配置類

/**
 * Shiro配置中心
 *
 * @Auther: hrabbit
 * @Date: 2018-12-24 12:33 PM
 * @Description:
 */
@Configuration
public class ShiroConfig {


    /**
     * Shiro的過濾器鏈
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        /**
         * 默認登錄路徑
         */
        shiroFilter.setLoginUrl("/login");

        /**
         * 登錄成功後要跳轉的鏈接
         */
        shiroFilter.setSuccessUrl("/");
        /**
         * 沒有權限的時候跳轉頁面
         */
        shiroFilter.setUnauthorizedUrl("/global/error");
        /**
         * 配置shiro攔截器鏈
         *
         * anon  不需要認證
         * authc 需要認證
         * user  驗證通過或RememberMe登錄的都可以
         *
         * 當應用開啓了rememberMe時,用戶下次訪問時可以是一個user,但不會是authc,因爲authc是需要重新認證的
         *
         * 順序從上到下,優先級依次降低
         *
         * api開頭的接口,走rest api鑑權,不走shiro鑑權
         *
         */
        Map<String, String> hashMap = new LinkedHashMap<>();
        hashMap.put("/static/**", "anon");
        hashMap.put("/login", "anon");
        hashMap.put("/global/sessionError", "anon");
        hashMap.put("/**", "user");
        shiroFilter.setFilterChainDefinitionMap(hashMap);
        return shiroFilter;
    }

    /**
     * 憑證匹配器
     * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了
     * )
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這裏使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 md5(md5(""));
        return hashedCredentialsMatcher;
    }

    /**
     * 自定義shiro認證、授權
     *
     * @return
     */
    @Bean
    public ShiroRealm shiroDbRealm() {
        ShiroRealm shiroDbRealm = new ShiroRealm();
        shiroDbRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroDbRealm;
    }
}

注意:裏面的 SecurityManager 類導入的應該是 import org.apache.shiro.mgt.SecurityManager;

shirFilter 方法中主要是設置了一些重要的跳轉 url,比如未登陸時setLoginUrl,無權限時的跳轉setUnauthorizedUrl

權限攔截 Filter

當運行一個Web應用程序時,Shiro將會創建一些有用的默認 Filter 實例,並自動地將它們置爲可用,而這些默認的 Filter 實例是被 DefaultFilter 枚舉類定義的,當然我們也可以自定義 Filter 實例

Filter 解釋
anon 無參,開放權限,可以理解爲匿名用戶或遊客
authc 無參,需要認證
logout 無參,註銷,執行後會直接跳轉到shiroFilterFactoryBean.setLoginUrl(); 設置的 url
authcBasic 無參,表示 httpBasic 認證
user 無參,表示必須存在用戶,當登入操作時不做檢查
ssl 無參,表示安全的URL請求,協議爲 https
perms[user] 參數可寫多個,表示需要某個或某些權限才能通過,多個參數時寫 perms["user, admin"],當有多個參數時必須每個參數都通過纔算通過
roles[admin] 參數可寫多個,表示是某個或某些角色才能通過,多個參數時寫 roles["admin,user"],當有多個參數時必須每個參數都通過纔算通過
rest[user] 根據請求的方法,相當於 perms[user:method],其中 method 爲 post,get,delete 等
port[8081] 當請求的URL端口不是8081時,跳轉到schemal://serverName:8081?queryString 其中 schmal 是協議 http 或 https 等等,serverName 是你訪問的 Host,8081 是 Port 端口,queryString 是你訪問的 URL 裏的 ? 後面的參數

常用的主要就是 anon,authc,user,roles,perms 等

注意:anon, authc, authcBasic, user 是第一組認證過濾器,perms, port, rest, roles, ssl 是第二組授權過濾器,要通過授權過濾器,就先要完成登陸認證操作(即先要完成認證才能前去尋找授權) 才能走第二組授權器(例如訪問需要 roles 權限的 url,如果還沒有登陸的話,會直接跳轉到 shiroFilterFactoryBean.setLoginUrl(); 設置的 url

自定義 realm 類

我們首先要繼承 AuthorizingRealm 類來自定義我們自己的 realm 以進行我們自定義的身份,權限認證操作。

/**
 * 自定義Shiro規則
 * @Auther: hrabbit
 * @Date: 2018-11-21 1:16 PM
 * @Description:
 */
@Slf4j
public class MyShiroRealm extends AuthorizingRealm {

    @Resource
    private SysModuleOperationService sysModuleOperationService;

    @Resource
    private SysUsersService sysUsersService;

    @Resource
    private SysRolesService sysRolesService;

    /**
     * 資源認證
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        ShiroUser userInfo  = (ShiroUser) principals.getPrimaryPrincipal();
        //按鈕資源
        Set<String> permissionSet = new HashSet<>();
        //用戶角色
        Set<String> roleNameSet = new HashSet<>();
        
        //獲取用戶的角色集合
        List<Integer> roleList = userInfo.getRoleList();

        for (Integer roleId:roleList){
            //根據角色id獲取到資源信息
            List<ModuleOperation> allMenuByUserId = sysModuleOperationService.getPermissionByRoleId(roleId);
            for (ModuleOperation moduleOperation:allMenuByUserId){
                if (ToolUtil.isNotEmpty(moduleOperation.getCode()))
                permissionSet.add(moduleOperation.getCode());
            }
            //查詢角色信息
            Roles roles = sysRolesService.selectById(roleId);
            if (roles!=null && ToolUtil.isNotEmpty(roles.getRoleCode())){
                roleNameSet.add(roles.getRoleCode());
            }
        }
        //添加按鈕資源
        authorizationInfo.addStringPermissions(permissionSet);
        //添加角色
        authorizationInfo.addRoles(roleNameSet);
        return authorizationInfo;
    }

    /**
     * 主要是用來進行身份認證的,也就是說驗證用戶輸入的賬號和密碼是否正確。
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        //獲取shiroFactory工廠
        ShiroFactoryService shiroFactory = ShiroFactroy.me();
        //獲取到用戶的信息
        UsernamePasswordToken userToken = (UsernamePasswordToken)token;
        //獲取用戶的輸入的賬號.
        String username = userToken.getUsername();
        //實際項目中,這裏可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
        ShiroUser userInfo = sysUsersService.getShiroUserByLoginName(username);
        SysUsers sysUser = sysUsersService.getSysUsersByLoginName(username);
        //創建緩存用戶信息
        SimpleAuthenticationInfo info = shiroFactory.info(userInfo,sysUser,super.getName());
        return info;
    }

    /**
     * 設置認證加密方式
     */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher();
        md5CredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName);
        md5CredentialsMatcher.setHashIterations(ShiroUtils.hashIterations);
        super.setCredentialsMatcher(md5CredentialsMatcher);
    }
}

重寫的兩個方法分別是實現身份認證以及權限認證,shiro 中有個作登陸操作的 Subject.login()方法,當我們把封裝了用戶名,密碼的 token 作爲參數傳入,便會跑進這兩個方法裏面(不一定兩個方法都會進入)
其中 doGetAuthorizationInfo方法只有在需要權限認證時纔會進去,比如前面配置類中配置了 filterChainDefinitionMap.put("/**", "user"); 的管理員角色,這時進入系統時就會進入 doGetAuthorizationInfo 方法來檢查權限;而 doGetAuthenticationInfo 方法則是需要身份認證時(比如前面的 Subject.login()方法)纔會進入
再說下 UsernamePasswordToken 類,我們可以從該對象拿到登陸時的用戶名和密碼(登陸時會使用 new UsernamePasswordToken(username, password);),而 get 用戶名或密碼有以下幾個方法

//獲得用戶名 String
token.getUsername();
//獲得用戶名 Object 
token.getPrincipal();
//獲得密碼 char[]
token.getPassword();
//獲得密碼 Object
token.getCredentials();

LoginController的實現


/**
 * 登錄控制器
 * @Auther: hrabbit
 * @Date: 2018-11-19 10:23 AM
 * @Description:
 */
@Controller
@Slf4j
@Api(value = "登錄API",description = "登錄、登出驗證,跳轉主界面")
public class LoginController extends BaseController {

    /**
     * 基礎路徑
     */
    private static String BASEURL = "modual";

    @Autowired
    private SysModuleOperationService sysModuleOperationService;

    @Autowired
    private SysUsersService sysUsersService;

    /**
     * 跳轉到主頁
     * @return
     */
    @RequestMapping(value = {"/","/index"},method = RequestMethod.GET)
    @ApiOperation(value="跳轉到主界面", notes="跳轉到主頁面,查詢用戶角色信息和頁面信息")
    public String index(ModelMap model){
        //獲取用戶角色idf
        List<Integer> roleList = ShiroUtils.getUser().getRoleList();
        //如果用戶不存在角色,跳轉到登錄界面
        if (roleList == null || roleList.size() == 0){
            ShiroUtils.getSubject().logout();
            model.addAttribute("msg","該用戶沒有角色,無法登陸");
            return "login";
        }
        //根據角色id查詢按鈕資源
        List<MenuNode> menuNodes = sysModuleOperationService.getAllMenuByRoleId(roleList);
        menuNodes = MenuNode.buildTitle(menuNodes);
        //返回用戶資料信息
        ShiroUser shiroUser = ShiroUtils.getUser();
        //將Shiro用戶信息返回到前端頁面
        model.addAttribute("user",shiroUser);
        model.addAttribute("title",menuNodes);
        return BASEURL+"/index.html";
    }

    /**
     * 跳轉到登錄界面
     * @return
     */
    @RequestMapping(value = "login",method = RequestMethod.GET)
    @ApiOperation(value="跳轉到登錄界面", notes="跳轉到登錄界面")
    public String login(){
        if (ShiroUtils.isAuthenticated() || ShiroUtils.getUser()!=null){
            return REDIRECT+ "/";
        }else{
            return "login.html";
        }
    }

    /**
     * 頁面提交登錄
     *
     * @param username 登錄名稱
     * @param password 用戶密碼
     * @return
     */
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    @ApiOperation(value="表單驗證", notes="提交登錄信息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "username",value = "用戶名稱",required = true,dataType = "String"),
            @ApiImplicitParam(name = "password",value = "用戶密碼",required = true,dataType = "String")
    })
    public String login(String username,String password){
         Subject subject = ShiroUtils.getSubject();
        //檢驗用戶是否存在
        SysUser sysUser = sysUserService.findByLoginName(username);
        // 在認證提交前準備 token(令牌)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 執行認證登陸
        subject.login(token);
        ShiroUser shiroUser = ShiroUtils.getUser();
        //將ShiroUser對象存儲到session中
        HttpUtils.getRequest().getSession().setAttribute("shiroUser",shiroUser);
        //保存Session狀態
        ShiroUtils.getSession().setAttribute("sessionFlag",true);
        return REDIRECT+"/";
    }


    /**
     * 退出登錄
     * @return
     */
    @RequestMapping(value = "loginOut",method = RequestMethod.GET)
    @ApiOperation(value="退出登錄", notes="返回登錄界面")
    public String loginOut(){
        ShiroUtils.getSubject().logout();
        return REDIRECT+"login.html";
    }
}

這裏我們需要注意創建異常攔截器,這樣當用戶名或者密碼不正確的時候,Shiro會自動拋出異常,我們只需要將異常捕獲即可

/**
 * 異常類
 *
 * @Auther: hrabbit
 * @Date: 2018-11-15 3:40 PM
 * @Description:
 */
@ControllerAdvice("com.hrabbit.admin")
@Order(-1)
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 其他異常拋出信息
     *
     * @param response
     * @param ex
     * @return
     */
    @ExceptionHandler(Exception.class)
    public BaseResponse otherExceptionHandler(HttpServletResponse response, Exception ex) {
        response.setStatus(500);
        log.error(ex.getMessage(), ex);
        return new BaseResponse(500, ex.getMessage());
    }


    /**
     * 賬號被凍結異常
     */
    @ExceptionHandler(DisabledAccountException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String accountLocked(DisabledAccountException e, Model model) {
        model.addAttribute("message", "賬號被凍結");
        return "/login.html";
    }

    /**
     * 賬號密碼錯誤異常
     */
    @ExceptionHandler(CredentialsException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String credentials(CredentialsException e, Model model) {
        model.addAttribute("message", "賬號密碼錯誤");
        return "/login.html";
    }
}

測試

密碼錯誤的時候,會自動捕獲到異常信息



密碼正確,進入到主頁面


代碼正在編寫中,等這塊我編寫完成,會放到碼雲上面的
碼雲地址: https://gitee.com/hrabbit/hrabbit-admin
個人博客:http://www.hrabbit.xin

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