Apache Shiro 一個簡單的Java安全框架,在此框架基礎上可以輕鬆實現網站的用戶認證和授權
- Shiro的三大核心對象:
- Subject (代表當前用戶對象)
- ShiroSecurityManager(管理所有Subject對象)
- Realm(管理數據:具體數據的授權與認證)
一、用戶認證與攔截器
1.SpringBoot項目準備
- 導入案例涉及依賴
<dependencies>
<!--mybatis druid數據原-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
<!--shiro spring-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--整合thymeleaf-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
- 配置Druid數據源,連接數據庫
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
#druid 數據源專有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
- 配置Mybatis別名,mapper位置
mybatis:
type-aliases-package: com.ht.springbootshiro01.pojo
mapper-locations: classpath:mapper/*.xml
# 資源位置resources/mapper/*.xml
-
編寫Dao、Service層 測試Mybatis正常使用
(1)Dao(Mapper)層邏輯代碼(省略實體類)
@Repository
@Mapper
// Dao 層
public interface UserMapper {
/**
* 根據用戶名 查詢指定用戶
* @param name
* @return
*/
public User queryUserByName(@Param("name") String name);
}
(2)Mapper.xml文件編寫(查詢數據庫)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ht.springbootshiro01.mapper.UserMapper">
<select id="queryUserByName" parameterType="String" resultType="User">
select * from mybatis.user where name=#{name};
</select>
</mapper>
(3)Service層業務邏輯(調用Dao層)
@Service
public class UserService {
// service 調用 dao層
@Autowired
UserMapper userMapper;
public User queryUserByName(String name){
return userMapper.queryUserByName(name);
}
}
(4)測試代碼(查詢用戶結果)
@SpringBootTest
class SpringbootShiro01ApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
// mybatis 測試
User admin = userService.queryUserByName("admin");
System.out.println(admin.toString());
}
}
至此查詢成功後,SpringBoot 整合MyBatis數據庫準備工作完成
2.基礎網頁路由配置
-
首頁,登錄頁,管理員權限下的顯示所有用戶網頁
- 跳轉路由代碼(Controller)
/**
* 管理路由跳轉的Controller
*/
@Controller
public class RouteController {
@RequestMapping({"/index","/"})
public String index(Model model){
model.addAttribute("msg","Hello Shiro");
return "index";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
//user
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/update")
public String update(){
return "user/update";
}
// show
@RequestMapping("/show")
public String show(){
return "show";
}
}
3.編寫Shiro配置文件,自定義Realm(管理授權、認證)
Shiro配置文件 配置三大要素:自定義Realm規則,SecurityManager安全管理對象,Shiro Filter攔截器相關信息配置
-
shiro的內置過濾器:
* anon: 無需認證就可訪問 * authc: 認證後纔可訪問 * user: 必須擁有記住我功能纔可訪問 * perms:擁有對某個資源的權限纔可訪問 * role: 擁有某個角色的權限纔可訪問
@Configuration
public class ShiroConfig {
//1.創建Realm對象 自定義 注入bean
@Bean(name = "getUserRealm")
public UserRealm getUserRealm(){
return new UserRealm();
}
//2.SecurityManager Spring 注入參數 bean Qualifier(方法名)
@Bean(name = "getSecurityManager")
public DefaultWebSecurityManager defaultSecurityManager(@Qualifier("getUserRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 關聯 Realm
securityManager.setRealm(userRealm);
return securityManager;
}
//3.Shiro Filter
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("getSecurityManager") DefaultSecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 設置 安全管理
bean.setSecurityManager(securityManager);
//過濾器配置
Map<String, String> filterMap = new LinkedHashMap<>();
// /user 請求 攔截 認證後可訪問
filterMap.put("/user/**","authc");
// 開放登錄接口 首頁
filterMap.put("/toLogin","anon");
filterMap.put("/index","anon");
filterMap.put("/","anon");
// 關閉其它路由 需授權訪問
//filterMap.put("/**","authc");
bean.setFilterChainDefinitionMap(filterMap);
//System.out.println("Shiro攔截器注入成功");
// 自定義登錄頁面路徑 實現登錄攔截
bean.setLoginUrl("/toLogin");
return bean;
}
}
至此實現了Shiro的初步登錄攔截,發現user/下的路由都不能訪問
4.編寫自定義Realm規則,認證 繼承AuthorizingRealm
~ token令牌實際應用規則:獲取前端登錄傳遞的用戶名、密碼信息封裝令牌,Shiro框架會自動經過自定義Realm 認證規則檢驗,與數據庫中用戶信息匹配比較
// 自定義UserRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("執行了 授權 ==》" + principals.getRealmNames().toString());
return null;
}
// token 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("執行了 認證 ==》" + token.getPrincipal().toString());
// 數據庫取數據 進行認證
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// 前端傳遞 用戶名 查詢 數據庫
User user = userService.queryUserByName(userToken.getUsername());
if (null == user){
// 前端傳遞 與 數據庫查詢用戶名 不一致
return null; // 拋出異常 未知用戶名
}
// 默認密碼不加密(不安全) 可以做 md5加密 或 md5鹽值加密
// 密碼認證 shiro做
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
}
- 登錄請求處理:獲取前端表單傳遞數據,封裝令牌==》執行登錄操作==》處理登錄異常
@Controller
public class LoginController {
/**
* 登錄請求 獲取表單傳遞數據 封裝令牌 token
* @param username
* @param password
* @param model
* @return
*/
@PostMapping("/login.do")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Model model){
System.out.println("登錄信息:username: " + username + " ; password: " + password);
// 獲取當前用戶
Subject user = SecurityUtils.getSubject();
// 封裝令牌 用於 Realm驗證
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
// 執行登錄
try {
user.login(token); // 調用自定義 UserRealm 認證方法
return "index";
} catch (UnknownAccountException uae) {
// 用戶不存在
System.out.println("There is no user with username of " + token.getPrincipal());
model.addAttribute("msg","用戶名不存在");
return "login";
} catch (IncorrectCredentialsException ice) {
// 密碼錯誤
System.out.println("Password for account " + token.getPrincipal() + " was incorrect!");
model.addAttribute("msg","密碼錯誤");
return "login";
} catch (LockedAccountException lae) {
// 當前用戶鎖定
System.out.println("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
model.addAttribute("msg","用戶鎖定");
return "login";
} catch (AuthenticationException ae) {
//unexpected condition? error?
System.out.println("其它錯誤");
model.addAttribute("msg","其它錯誤");
return "login";
}
}
}
~ 測試Shiro的用戶認證
二、用戶授權與前端整合
1.FilterMap中添加指定路由權限
Map<String, String> filterMap = new LinkedHashMap<>();
// 管理員開放權限
//filterMap.put("/user/**","perms[admin]");
// 授權 只有 擁有 user:add的權限纔可訪問 失敗401 未授權
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
// /user 請求 攔截 認證後可訪問
filterMap.put("/user/**","authc");
// 開放登錄接口 首頁
filterMap.put("/toLogin","anon");
filterMap.put("/index","anon");
filterMap.put("/","anon");
// 關閉其它路由 需授權訪問
//filterMap.put("/**","authc");
bean.setFilterChainDefinitionMap(filterMap);
//System.out.println("Shiro攔截器注入成功");
// 自定義登錄頁面路徑 實現登錄攔截
bean.setLoginUrl("/toLogin");
// 設置未授權請求
bean.setUnauthorizedUrl("/unauthorized");
2.編寫定製未授權跳轉頁面及路由控制器
/***
* 未授權控制器
*/
@Controller
public class UnauthorizedController {
/**
* 未授權頁面
* @return
*/
@RequestMapping("/unauthorized")
public String toUnauthorized(){
return "error/unauthor";
}
}
*自定義未授權頁面 /error/unauthor.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>未授權</title>
</head>
<body>
<div>
<h3 style="color: darkslategrey">未授權,請
<a th:href="@{/toLogin}">切換用戶</a>,或進行授權操作</h3>
</div>
</body>
</html>
3.根據數據庫中用戶權限 爲用戶授權
- 自定義Realm 認證 傳遞給授權 攜帶(Principal == 令牌負責人即登錄用戶)
- 授權方法 取出負責人用戶,根據數據庫中用戶權限授權
// 自定義UserRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("執行了 授權 ==》" + principals.getRealmNames().toString());
// 增加授權 所有用戶都賦予權限
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Subject subject = SecurityUtils.getSubject();
User principal = (User) subject.getPrincipal();
// 通過數據庫字段 增加 權限
info.addStringPermission(principal.getPerms());
return info;
}
// token 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("執行了 認證 ==》" + token.getPrincipal().toString());
// 數據庫取數據 進行認證
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// 前端傳遞 用戶名 查詢 數據庫
User user = userService.queryUserByName(userToken.getUsername());
if (null == user){
// 前端傳遞 與 數據庫查詢用戶名 不一致
return null; // 拋出異常 未知用戶名
}
// 密碼認證 shiro做 登錄成功後將用戶信息 傳遞給授權
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}
4.增加前端顯示用戶信息 與 註銷功能
- 首頁
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首頁</title>
</head>
<body>
<div>
<h1>首頁</h1>
<!---->
<p th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
<!--用戶信息-->
<p th:text="${user_name}"
th:if="${not #strings.isEmpty(user_name)}"></p>
</div>
<div>
<a th:href="@{/user/add}">增加</a>
<a th:href="@{/user/update}">修改</a>
<a th:href="@{/show}">展示</a>
<a th:href="@{/loginout.do}">註銷</a>
</div>
</body>
</html>
- 註銷控制器
@RequestMapping("/loginout.do")
public String loginOut(){
Subject subject = SecurityUtils.getSubject();
// 註銷
subject.logout();
return "login";
}
- 登錄成功 封裝用戶名
user.login(token); // 調用自定義 UserRealm 認證方法
model.addAttribute("user_name",token.getUsername());
return "index";
三、拓展整合Thymeleaf引擎
- 實現不同權限的用戶顯示不同操作
- 實現未登錄用戶 顯示 登錄按鈕;已登錄顯示註銷按鈕
重要標籤:
<!--頭引用-->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
shiro:hasPermission="xxx" 擁有某權限
shiro:guest="true" 未登錄
shiro:authenticated="true" 已登陸
- 完整首頁代碼
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>首頁</title>
</head>
<body>
<div>
<h1>首頁</h1>
<!---->
<p th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
<!--用戶信息-->
<p th:text="${user_name}"
th:if="${not #strings.isEmpty(user_name)}"></p>
</div>
<div>
<!--整合 shiro 根據權限顯示-->
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">增加</a>
</div>
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">修改</a>
</div>
<a th:href="@{/show}">展示</a>
<!--遊客顯示登錄按鈕 登錄成功不顯示-->
<div shiro:guest="true">
<a th:href="@{/toLogin}">登錄</a>
</div>
<div shiro:authenticated>
<a th:href="@{/loginout.do}">註銷</a>
</div>
</div>
</body>
</html>
~體驗到Shiro框架的便捷之處,告別過濾器