一、引入相關依賴
後面兩個依賴可以不引入,還沒有使用過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標籤
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實現會話跟蹤