SpringBoot - 整合Shiro

一、引入相關依賴

後面兩個依賴可以不引入,還沒有使用過Redis來做Shiro的緩存。後續如果有用到,可能會更新到博文。

<!-- Shiro -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>
<!-- Shiro-Thymeleaf -->
<dependency>
	<groupId>com.github.theborakompanioni</groupId>
	<artifactId>thymeleaf-extras-shiro</artifactId>
	<version>2.0.0</version>
</dependency>
<!-- Thymeleaf -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- Redis緩存Shiro - 本文未使用,可不引入 -->
<dependency>
	<groupId>org.crazycake</groupId>
	<artifactId>shiro-redis</artifactId>
	<version>2.4.2.1-RELEASE</version>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-ehcache</artifactId>
	<version>1.4.0</version>
</dependency>

二、基礎準備

1、創建用戶類(User)
@Data
public class User {
    /** 主鍵Id */
    private Long id;
    /** 賬號 */
    private String username;
    /** 密碼 */
    private String password;

    private List<Role> roles;
}
2、創建Service層(UserService)
@Service
public class UserServiceImpl implements UserService {

    @Override
    public User getUserByUsername(String username) {
        User user = new User();
        // 應該從數據庫獲取,這裏寫死了
        user.setId(1L);
        user.setUsername("lcy123456");
        user.setPassword("97fe5ea8b72e6a39bd9e500cb462e426");
        return user;
    }

    @Override
    public List<String> getRolesById(Long id) {
        List<String> roles = new ArrayList<>();
        // 應該從數據庫獲取,這裏寫死了
        roles.add("hr");
        roles.add("manager");
        return roles;
    }

    @Override
    public List<String> getPermissionById(Long id) {
        List<String> permissions = new ArrayList<>();
        // 應該從數據庫獲取,這裏寫死了
        permissions.add("role:index");
        permissions.add("menu:index");
        return permissions;
    }
}
3、靜態登錄頁準備

登錄頁面
添加按鈕在這裏的意義是本來想演示:通過這個按鈕發起請求,但是沒有這個權限的解決辦法。(實際沒啥用,因爲我這裏沒有權限是通過@ControllerAdvice做了統一處理),只需關注登錄即可。

三、自定義AuthorizingRealm

創建一個類繼承自AuthorizingRealm,這個類的作用就是用來認證與授權的。

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    /**
     * 身份認證
     * 前端form表單通過post請求發送的/login請求
     * 會自動將name爲username和password的值放到token裏去
     * 當然也可以自己手動提交到token裏去 - 本例的做法
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 從Token中獲取賬戶
        String username = (String) authenticationToken.getPrincipal();
        // String password = new String((char[]) authenticationToken.getCredentials());  // 獲取密碼
        // 直接根據獲取到的username去數據庫登錄用戶對象
        User user = userService.getUserByUsername("lcy123456");
        if(user == null){
            throw new AccountException("用戶名或密碼錯誤!");
        }
        // 交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配
        // 參數:主體、正確的密碼、鹽、當前realm名稱
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                ByteSource.Util.bytes(user.getUsername()),
                this.getName()
        );
        return info;
    }

    /**
     * 用戶授權
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 獲取用戶身份信息
        User user = (User) principalCollection.getPrimaryPrincipal();
        // 根據當前角色去查詢權限
        List<String> roles = userService.getRolesById(user.getId());
        List<String> permissions = userService.getPermissionById(user.getId());
        // 給授權信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(roles);
        info.addStringPermissions(permissions);
        return info;
    }
}

四、Shiro配置文件

@Configuration
public class ShiroConfig {

    /**
     * 配置ShiroDialect,用於Shiro和thymeleaf標籤配合使用
     * 可以讓Thymealf頁面使用shiro標籤
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }

    /**
     * 將自定義Realm交給Spring管理
     * @return UserRealm
     */
    @Bean
    public UserRealm userRealm(){
        UserRealm userRealm = new UserRealm();
        // 告訴Realm,使用credentialsMatcher加密算法類來驗證密文
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        // 設置不允許緩存
        userRealm.setCachingEnabled(false);
        // userRealm.setAuthenticationCachingEnabled(true);   // 允許認證緩存
        // userRealm.setAuthenticationCacheName("authenticationCache");
        // userRealm.setAuthorizationCachingEnabled(true);    // 允許授權緩存
        // userRealm.setAuthorizationCacheName("authorizationCache");
        return userRealm;
    }

    /**
     * Shiro核心類:協調Shiro內部的各種安全組件
     * @return SecurityManager
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 設置realm
        securityManager.setRealm(userRealm());
//        // 自定義緩存實現 - 使用Redis
//        securityManager.setCacheManager(cacheManager());
//        // 自定義session管理 - 使用redis
//        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    
    /**
     * Shiro過濾器 - 訪問權限控制
     * @param securityManager
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 設置登錄請求url  - 註銷的url是/logout
        shiroFilterFactoryBean.setLoginUrl("/login");
//        shiroFilterFactoryBean.setSuccessUrl("/");  // 成功跳轉地址
//        // 無權限跳轉的請求 - 註解鑑權,這個不會生效
//        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        /* *
         * 過濾鏈定義
         * 設置訪問權限 - Map使用LinkedList,因爲它是有順序的
         * authc:需要驗證的url   anno:無需驗證的url
         */
        Map<String,String> filterChainMap = new LinkedHashMap<>();
        // 配置某個url需要某個權限碼 - 通常用註解的方式
        // filterChainMap.put("/hello", "perms[how_are_you]");
        // 過濾掉靜態文件css/js/images - Thymeleaf的靜態文件一般放在resources/static下的
        filterChainMap.put("/css/**","anno");
        filterChainMap.put("/js/**","anno");
        filterChainMap.put("/images/**","anno");
        filterChainMap.put("/lib/**","anno");
        // 登錄、註冊、錯誤不需要驗證
        filterChainMap.put("/login","anno");
        filterChainMap.put("/register","anno");
        filterChainMap.put("/error","anno");
        // 需要攔截驗證的url
        filterChainMap.put("/admin/**","authc");
        filterChainMap.put("/user/**","authc");
        // 下面這行攔截所有代碼必須在Map最後一個,否則會攔截所有url
        // 這樣寫的話,就是除了前面無需驗證的,剩下的全都得驗證
        filterChainMap.put("/**","authc");
        // 注意:認證成功/失敗也可以通過shiro的過濾器來做 - FormAuthenticationFilter
        return shiroFilterFactoryBean;
    }

    /**
     * 憑證匹配器 - 匹配密碼的規則
     * @return
     */
    @Bean(name = "credentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 使用MD5算法
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列此時 - 2次
        hashedCredentialsMatcher.setHashIterations(2);
        // 設置存儲的憑證編碼,默認爲true:即Hex,如果爲false,則爲Base64編碼
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /************************************** 開啓註解配置權限start ****************************************/
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 默認的代理創建者
     * 開啓Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證
     * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能
     * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 授權源
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
    /************************************** 開啓註解配置權限end ****************************************/


    /************************************** 開啓Redis緩存start ****************************************/

//    /**
//     * cacheManager 緩存 redis實現
//     * 使用的是shiro-redis開源插件
//     * @return
//     */
//    public RedisCacheManager cacheManager() {
//        RedisCacheManager redisCacheManager = new RedisCacheManager();
//        redisCacheManager.setRedisManager(redisManager());
//        return redisCacheManager;
//    }
//    /**
//     * 配置shiro redisManager
//     * 使用的是shiro-redis開源插件
//     *
//     * @return
//     */
//    @Bean
//    public RedisManager redisManager() {
//        RedisManager redisManager = new RedisManager();
////        redisManager.setHost(host);
////        redisManager.setPort(port);
////        // 配置緩存過期時間
////        redisManager.setExpire(expireTime);
////        redisManager.setTimeout(timeOut);
////         redisManager.setPassword(password);
//        return redisManager;
//    }
//
//    /**
//     * Session Manager
//     * 使用的是shiro-redis開源插件
//     */
//    @Bean
//    public DefaultWebSessionManager sessionManager() {
//        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//        sessionManager.setSessionDAO(redisSessionDAO());
//        return sessionManager;
//    }
//
//    /**
//     * RedisSessionDAO shiro sessionDao層的實現 通過redis
//     * 使用的是shiro-redis開源插件
//     */
//    @Bean
//    public RedisSessionDAO redisSessionDAO() {
//        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
//        redisSessionDAO.setRedisManager(redisManager());
//        return redisSessionDAO;
//    }
    /************************************** 開啓Redis緩存end ****************************************/
}

因爲做了密碼加密,所以註冊的時候也需要對密碼加密,如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest
public class Test1 {
    @Test
    public void test(){
    	// MD5算法,對123456進行加鹽並進行2次散列
        String md5Pwd = new SimpleHash("MD5", "123456",
                ByteSource.Util.bytes("lcy123456"), 2).toHex();
        System.out.println(md5Pwd);
    }
}

五、Thymeleaf頁面使用Shiro標籤

要想在thymeleaf使用shiro的標籤,需要引入2.0以上的thymeleaf-extras-shiro依賴

<!DOCTYPE html>
<!-- 需加入對應的命名空間 -->
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="/logout">註銷</a>
<h1 shiro:hasPermission="role:index">role:index</h1>
<h1 shiro:hasPermission="role:indexAbc">role:indexAbc</h1>
<h1 shiro:hasRole="manager">manager</h1>
<h1 shiro:hasRole="abc">abc</h1>
</body>
</html>

六、Controller的書寫

Controller層的書寫主要是演示手動提交token和手動註銷,以及權限註解RequiresPermissions的使用。

@Controller
public class ReController {
    /**
     * 返回JSON數據 - 跳轉交給前端來做,這裏只是示例
     * 但是似乎前端沒有辦法跳轉到templates下的頁面
     * 根據業務做,可以不返回Json數據,直接進行跳轉即可
     * 這裏因爲前端使用的是axios/ajax請求,自己手動提交token
     * @param username 賬號
     * @param password 密碼
     * @return
     */
    @PostMapping("/login")
    @ResponseBody
    public String Login(@RequestParam("username") String username, @RequestParam("password") String password){
        // 從SecurityUtils裏邊創建一個 subject
        Subject subject = SecurityUtils.getSubject();
        // 在認證提交前準備 token(令牌)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 執行認證登陸
        try {
            subject.login(token);
        } catch (UnknownAccountException uae) {
            return "未知賬戶";
        } catch (IncorrectCredentialsException ice) {
            return "密碼不正確";
        } catch (LockedAccountException lae) {
            return "賬戶已鎖定";
        } catch (ExcessiveAttemptsException eae) {
            return "用戶名或密碼錯誤次數過多";
        } catch (AuthenticationException ae) {
            return "用戶名或密碼不正確!";
        }
        if (subject.isAuthenticated()) {
            return "登錄成功";
        } else {
            token.clear();
            return "登錄失敗";
        }
    }

    /**
     * 這個就是針對上面那個返回JSON的反面例子
     * 直接跳轉到templates下的loginhtml.html
     * 這裏只是給大家兩種思路:可以直接返回頁面,也可以讓前端來跳轉
     * @return
     */
    @GetMapping("/logout")
    public String logout(){
        Subject lvSubject=SecurityUtils.getSubject();
        // 註銷
        lvSubject.logout();
        return "loginhtml";
    }

    /**
     * templates下的/loginhtml.html頁面(登錄頁面)
     * @return
     */
    @GetMapping("/")
    public String index(){
        return "loginhtml";
    }


    @GetMapping("/index")
    public String indexTo(){
        return "index";
    }


    @GetMapping("/admin/add")
    @RequiresPermissions("user:list")
    public String add(){
        return "新增成功!";
    }

    @GetMapping("/role/index")
    @RequiresPermissions("role:index")
    public String addRole(){
        return "新增角色成功!";
    }

    @RequiresRoles("admin")
    @GetMapping("/admin")
    public String admin(){
    	return "角色admin可以訪問!";
    ]
	@RequiresRoles(value = {"admin","user"},logical = Logical.OR)
    @GetMapping("/user")
    public String user(){
    	return "擁有admin或user角色可以訪問!";
    }
}

七、無權限異常處理

@ControllerAdvice
public class ControllerExceptionHandler {
    @ExceptionHandler(AuthorizationException.class)
    public ModelAndView exceptionHandler(HttpServletRequest request, Exception e){
        ModelAndView mv = new ModelAndView();
        mv.setViewName("/403");
        return mv;
    }
}

以上代碼總結:

基本演示了前後端分離和非前後端分離的解決方案(不全),對於無權訪問也給出瞭解決方案,演示了Shiro標籤在Thymealf頁面上的使用。不足的是,因爲沒有具體的項目做支撐,因此很多地方想的不周全。這裏推薦一篇博文:SpringBoot項目+Shiro(權限框架)+Redis(緩存)集成
最後就是:文中所有的return跳轉頁面,都是跳轉的templates下的頁面。而使用ajax請求的則是通過前端進行跳轉的,則是在static下的頁面。

補充:Shiro標籤

Shiro標籤原文

guest標籤
  <shiro:guest>
  </shiro:guest>
  用戶沒有身份驗證時顯示相應信息,即遊客訪問信息。

user標籤
  <shiro:user>  
  </shiro:user>
  用戶已經身份驗證/記住我登錄後顯示相應的信息。

authenticated標籤
  <shiro:authenticated>  
  </shiro:authenticated>
  用戶已經身份驗證通過,即Subject.login登錄成功,不是記住我登錄的。

notAuthenticated標籤
  <shiro:notAuthenticated>
  
  </shiro:notAuthenticated>
  用戶已經身份驗證通過,即沒有調用Subject.login進行登錄,包括記住我
    自動登錄的也屬於未進行身份驗證。

principal標籤
  <shiro: principal/>
  
  <shiro:principal property="username"/>
  相當於((User)Subject.getPrincipals()).getUsername()。

lacksPermission標籤
  <shiro:lacksPermission name="org:create">
 
  </shiro:lacksPermission>
  如果當前Subject沒有權限將顯示body體內容。

hasRole標籤
  <shiro:hasRole name="admin">  
  </shiro:hasRole>
  如果當前Subject有角色將顯示body體內容。

hasAnyRoles標籤
  <shiro:hasAnyRoles name="admin,user">
   
  </shiro:hasAnyRoles>
  如果當前Subject有任意一個角色(或的關係)將顯示body體內容。

lacksRole標籤
  <shiro:lacksRole name="abc">  
  </shiro:lacksRole>
  如果當前Subject沒有角色將顯示body體內容。

hasPermission標籤
  <shiro:hasPermission name="user:create">  
  </shiro:hasPermission>
  如果當前Subject有權限將顯示body體內容

補充 - 使用Shiro的Starter

<!-- 無需添加spring-boot-starter-web,已經依賴了 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring-boot-web-starter</artifactId>
	<version>1.4.2</version>
</dependency>

用法和上面的基本一致,對於請求的過濾可以在ShiroConfig使用ShiroFilterChainDefinition代替ShiroFilterFactoryBean,示例:

@Bean 
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
	DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
	// 除了這些,其餘的全部都需要驗證
	chainDefinition.addPathDefinition("/css/**","anno");
	chainDefinition.addPathDefinition("/js/**","anno");
	chainDefinition.addPathDefinition("/images/**","anno");
	chainDefinition.addPathDefinition("/lib/**","anno");
	chainDefinition.addPathDefinition("/login","anno");
	chainDefinition.addPathDefinition("/register","anno");
    return chainDefinition;
}

可在yml中配置Shiro

shiro:
  enabled: true        # 開啓Shiro Web配置,默認爲true
  loginUrl: /login     # 登錄地址,默認爲/login.jsp
  successUrl: /index   # 登錄成功地址,默認爲/
  unauthorizedUrl: /unauthorized    # 無權限跳轉地址
  sessionManager:
    sessionIdCookieEnabled: true    # 是否允許通過URL參數實現會話跟蹤,默認爲true,如果網站支持Cookie,可以關閉選項
    sessionIdUrlRewritingEnabled: true  # 是否允許通過Cookie實現會話跟蹤
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章