shiro作爲安全框架,使用還是比較多,而且相對來說,比較容易上手。下面將使用springboot來整合shiro,實現的功能如下:
(1)實現訪問控制,未登錄時,只能訪問登錄接口
(2)實現角色和權限的訪問控制
示例代碼,已放在了GitHub上:https://github.com/qiuxinfa/shiro-study
先看下目錄結構:
1.maven依賴:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
2.創建一個用戶類User:
public class User {
private String username;
private String password;
private Integer enable; //賬號是否鎖定
//省略set、get、toString方法
}
3.創建一個業務查詢類,模擬數據存儲和數據查詢功能,因爲這裏沒有連接數據庫:
@Service
public class UserService {
//用來存儲用戶信息的map
private Map<String,User> userInfo = new HashMap<>(3);
public UserService(){
//用戶信息
userInfo.put("admin",new User("admin","123",1));
userInfo.put("sam",new User("sam","123",1));
userInfo.put("qxf",new User("qxf","123",0));
}
//根據用戶名,查找用戶,模擬數據庫查詢
public User getUserByUsername(String username){
return userInfo.get(username);
}
//獲取所有的用戶
public Map<String,User> getAllUser(){
return userInfo;
}
}
這裏爲了簡單起見,用一個map來存儲用戶信息,實際使用中,用戶信息肯定是存儲在數據庫中的。
4.自定義身份認證和授權的realm,繼承了AuthorizingRealm ,重寫認證和授權方法:
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User)SecurityUtils.getSubject().getPrincipal(); //獲取當前登錄的用戶信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//這裏模擬數據庫,進行角色和權限的處理
//這裏只是簡單模擬,角色使用是用戶名,權限是包含用戶名的路徑
simpleAuthorizationInfo.addRole(user.getUsername());
simpleAuthorizationInfo.addStringPermission("/"+user.getUsername());
return simpleAuthorizationInfo;
}
//認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
//這裏模擬數據庫查詢用戶,根據用戶名查詢
User dbUser = userService.getUserByUsername(username);
if (dbUser == null){
//賬號不存在
throw new UnknownAccountException();
}
if (dbUser.getEnable()==0){
//賬號被鎖定
throw new LockedAccountException();
}
return new SimpleAuthenticationInfo(dbUser, dbUser.getPassword(), getName());
}
}
5.配置shiro
/**
*shiro配置
*/
@Configuration
public class ShiroConfig {
@Bean
MyRealm myRealm() {
//返回自定義的身份認證和授權realm
return new MyRealm();
}
@Bean
SecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm());
return manager;
}
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager());
//設置登錄的url
bean.setLoginUrl("/login");
Map<String, String> map = new LinkedHashMap<>();
//anon表示,匿名訪問,不需要認證就可以訪問的接口
map.put("/doLogin", "anon");
//authc表示需要身份認證後纔可以訪問
map.put("/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
//下面配置的3個bean,主要是爲了Shiro的註解(如@RequiresRoles,@RequiresPermissions)生效
@Bean
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
這裏需要注意一下,如果想要使用shiro的註解生效,需要這3個bean:
(1)LifecycleBeanPostProcessor
(2)DefaultAdvisorAutoProxyCreator
(3)AuthorizationAttributeSourceAdvisor
6.定義訪問接口:
@Controller
public class ShiroController {
@GetMapping("/login")
public String login(){
return "login.html";
}
@PostMapping("/doLogin")
@ResponseBody
public String doLogin(String username,String password){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return "登錄成功";
}catch (UnknownAccountException|IncorrectCredentialsException e){
e.printStackTrace();
return "賬號或密碼錯誤";
}catch (LockedAccountException e){
e.printStackTrace();
return "賬號已被鎖定,請聯繫管理員";
}catch (AuthenticationException e){
e.printStackTrace();
return "未知異常,請聯繫管理員";
}
}
@GetMapping("/hello")
@ResponseBody
public String hello(){
return "hello:"+SecurityUtils.getSubject().getPrincipal();
}
}
這裏定義了3個接口:
(1)login,處理get請求,匿名可訪問,會跳轉到login.html登錄頁面
(2)doLogin,處理post請求,實現真正的登錄功能
(3)hello,獲取自身信息,認證後纔可以訪問
7.登錄頁面,寫得很簡陋,就是一個簡單HTML的form表單提交:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/doLogin" method="post">
用戶名:<input type="text" name="username" />
<br/>
密 碼:<input type="password" name="password"/>
<br/>
<input type="submit" value="登錄"/>
</form>
</body>
</html>
8.測試
(1)驗證這個功能:未登錄時,只能訪問登錄接口
寫個啓動類,啓動項目後,在瀏覽器輸入http://localhost:8080/hello,則會跳轉到登錄頁面,因爲在UserService裏面添加了3個用戶:
//用戶信息
userInfo.put("admin",new User("admin","123",1));
userInfo.put("sam",new User("sam","123",1));
userInfo.put("qxf",new User("qxf","123",0));
所以,當我們使用admin或者sam登錄時(密碼都是123),就可以登錄成功(qxf賬號是被鎖定了),登錄成功之後,再訪問hello就可以看到自己的信息了。
(2)驗證角色和權限訪問控制:
在MyRealm的授權方法中,我是用 用戶名作爲角色添加了,用 /+用戶名作爲權限,真實項目肯定不是這樣的,一般會配置在數據庫中,這裏只是爲了簡單起見:
//這裏模擬數據庫,進行角色和權限的處理
//這裏只是簡單模擬,角色使用是用戶名,權限是包含用戶名的路徑
simpleAuthorizationInfo.addRole(user.getUsername());
simpleAuthorizationInfo.addStringPermission("/"+user.getUsername());
添加只有admin可以訪問的接口:
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private UserService userService;
@GetMapping("/allUser")
@RequiresRoles("admin") //表示需要有[admin]這個角色纔可以訪問
@RequiresPermissions("/admin") //表示需要有 /admin 權限纔可以訪問
public Collection<User> allUser(){
List<User> list = new ArrayList<>();
Map<String,User> userMap = userService.getAllUser();
return userMap.values();
}
}
再寫一個全局異常處理,用以捕獲沒有權限訪問的異常:
/**
* @Auther: qiuxinfa
* @Date: 2020/4/2
* @Description: 定義全局異常處理
*/
@RestControllerAdvice
public class MyGlobalException {
//表示只捕獲AuthorizationException類及其子類異常
@ExceptionHandler(AuthorizationException.class)
public String handlerAuthorizationException(AuthorizationException e){
e.printStackTrace();
return "你沒有權限訪問";
}
}
現在,可以啓動項目了,如果用sam登錄後,訪問http://localhost:8080/admin/allUser,那麼就會返回如下信息:
你沒有權限訪問
後臺也會打印出,需要角色admin纔可以訪問,其實還需要/admin權限纔可以訪問
如果用admin登錄後,訪問http://localhost:8080/admin/allUser,則會返回所有的用戶信息:
[{"username":"admin","password":"123","enable":1},{"username":"sam","password":"123","enable":1},{"username":"qxf","password":"123","enable":0}]
這樣就實現了角色的訪問控制,權限也是一樣的道理,爲了簡單起見,demo中儘量返回的是json,只有一個登錄頁面。
其他注意點:
授權不是認證之後就立馬執行的,而是當你訪問某個資源需要權限時,纔會執行授權的方法,比如訪問添加了
@RequiresRoles,RequiresPermissions註解的時候,纔會觸發進入到我們自定義realm的授權方法。