章節目錄
1. Shiro
1.1 Shiro簡介
以下爲Shiro的簡單介紹:
- Shiro 是一個強大、簡單易用的 Java 安全權限框架
- Shiro 可以非常容易的開發出足夠好的應用,其不僅可以用在JavaSE 環境,也可以用在 JavaEE 環境
- Shiro 可以完成:認證、授權、加密、會話管理、與Web 集成、緩存等。
具體的信息可進一步查看Shiro官網
1.2 Shiro三大核心組件
Shiro 有三大核心組件,即 Subject、SecurityManager 和 Realm。它們的關係如下圖:
1.2.1 Subject
Subject 爲認證主體,它包含 Principals 和 Credentials 兩個信息。
- Principals:代表身份。可以是用戶名、郵件、手機號碼等等,用來標識一個登錄主體的身份
- Credentials:代表憑證。常見的有密碼,數字證書等等。
簡單來說:這兩者代表了需要認證的內容,最常見的便是用戶名、密碼了
1.2.2 SecurityManager
SecurityManager 爲安全管理員,它是Shiro 架構的核心。所有具體的交互都通過SecurityManager進行控制;負責所有Subject、且負責進行認證和授權、及會話、緩存的管理
1.2.3 Realm
Realm 是一個域,它是連接 Shiro 和具體應用的橋樑。當需要與安全數據交互時,比如用戶賬戶、訪問控制等,Shiro 將會在一個或多個 Realm 中查找。
1.3 Shiro的認證和授權流程
官方的入門實例:http://shiro.apache.org/tutorial.html
大致代碼如下:
shiro.ini
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
ini文件大致意思是:
- root=secret,admin:用戶名root,密碼secret,角色是admin
- schwartz=lightsaber:* :角色schwartz擁有權限 lightsaber:*
Java代碼
public static void main(String[] args) {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();
if ( !currentUser.isAuthenticated() ) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
currentUser.login(token);
}
if ( currentUser.hasRole( "schwartz" ) ) {
log.info("May the Schwartz be with you!" );
} else {
log.info( "Hello, mere mortal." );
}
if ( currentUser.isPermitted( "lightsaber:wield" ) ) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
...
}
上面代碼主要有兩個步驟:
認證:
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
currentUser.login(token);
判斷權限:
currentUser.hasRole( "schwartz" )
1.3.1 認證流程
shiro的認證流程如下:
- Subject進行login操作,參數是封裝了用戶信息的token
- Security Manager進行登錄操作
- Security Manager委託給Authenticator進行認證邏輯處理
- 調用AuthenticationStrategy進行多Realm身份驗證
- 調用對應Realm進行登錄校驗,認證成功則返回用戶屬性,失敗則拋出對應異常
1.3.2 授權流程
授權流程如下:
- 調用Subject.isPermitted()/hasRole()方法
- 委託給SecurityManager
- 而SecurityManager接着會委託給Authorizer
- Authorizer會判斷Realm的角色/權限是否和傳入的匹配
- 匹配如isPermitted/hasRole會返回true,否則返回false表示授權失敗
2. SpringBoot集成Shiro並完成登錄操作
2.1 項目信息
2.1.1 開發環境
IDEA:2018.2(lombok插件)
SpringBoot:2.3.1.RELEASE
Shiro:1.3.2
freemarker(前端頁面)
2.1.2 項目結構圖
2.1.3 pom依賴
<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.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
</dependencies>
2.2 集成Shiro
根據官方文檔 可以直接使用starter依賴包,但按照網上的搭建過程基本上是有坑的。所以,這裏就使用了shiro-spring的依賴包咯(可查看這篇博客)。
步驟:
1)、導入依賴包
上述有。
2)、修改配置文件application.yml
spring:
freemarker:
suffix: .ftl
template-loader-path: classpath:/templates/
settings:
#解決前臺使用${}賦值值爲空的情況
classic_compatible: true
這裏就是對前端頁面freemarker的配置。至於對shiro的配置,就是通過SpringBoot中的配置類進行的。
3)、配置Shiro
配置shiro的securityManager和自定義realm。因爲realm負責我們的認證與授權,所以是必須的,自定義的realm必須要交給securityManager管理,所以這兩個類需要重寫。然後還有一些資源的權限說明,所以一般需要定義ShiroFilterFactoryBean,所以有3個類我們常寫的:
- AuthorizingRealm:自定義realm
- DefaultWebSecurityManager:shiro的核心管理器
- ShiroFilterFactoryBean:過濾器鏈配置
4)、動手編碼
ShiroConfig:
@Slf4j
@Configuration
public class ShiroConfig {
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設置realm
securityManager.setRealm(userRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必須設置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 設置登錄頁面的url;如果不設置值,默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/login");
// 設置授權失敗時應該跳轉的頁面(即:訪問了權限不足的頁面時,需要跳轉到/role對應的url)
shiroFilterFactoryBean.setUnauthorizedUrl("/role");
shiroFilterFactoryBean.setSuccessUrl("/success");
// 設置攔截器
Map<String, String> filterMap = new LinkedHashMap<>();
// anon:表示可以匿名訪問(不需要認證)
filterMap.put("/guest/**", "anon");
// 用戶,需要角色權限 user
filterMap.put("/user/**", "roles[user]");
filterMap.put("/admin/**", "roles[admin]");
// 開放登陸接口
filterMap.put("/doLogin", "anon");
filterMap.put("/login", "anon");
// 其餘接口一律攔截
// 主要這行代碼必須放在所有權限設置的最後,不然會導致所有 url 都被攔截
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
log.info("Shiro攔截器工廠類注入成功");
return shiroFilterFactoryBean;
}
}
【注意】:SecurityManager 類導入的應該是 import org.apache.shiro.mgt.SecurityManager;
shirFilter()方法中主要設置了一些跳轉url(如:沒有登錄時,會跳轉到 /login 對應的頁面)以及各類url的權限攔截(如:訪問"/user"開頭的url,需要"user"的角色;訪問"/guest"開頭的url,可以匿名訪問)。
權限攔截 Filter
這裏對Shiro中的Filter稍加說明一下:當運行一個Web應用程序時,Shiro將會創建一些有用的默認 Filter 實例,並自動地將它們置爲可用,而這些默認的 Filter 實例是被 DefaultFilter 枚舉類定義的。
對其中一些過濾器稍加解釋一下:
Filter | 解釋 |
---|---|
anon | 無參,開放權限,可以理解爲匿名用戶或遊客 |
authc | 無參,需要認證 |
logout | 無參,註銷,執行後會直接跳轉到shiroFilterFactoryBean.setLoginUrl(); 設置的 url |
user | 無參,表示必須存在用戶,當登入操作時不做檢查 |
perms[user] | 參數可寫多個,表示需要某個或某些權限才能通過,多個參數時寫 perms[“user, admin”],當有多個參數時必須每個參數都通過纔算通過 |
roles[admin] | 參數可寫多個,表示是某個或某些角色才能通過,多個參數時寫 roles[“admin,user”],當有多個參數時必須每個參數都通過纔算通過 |
【注意】:anon, authc, authcBasic, user 是第一組認證過濾器;perms, port, rest, roles, ssl 是第二組授權過濾器,要通過授權過濾器,就先要完成登陸認證操作。即:先要完成認證才能前去尋找授權,才能走第二組授權器。如:訪問需要 roles 權限的 url,如果還沒有登陸的話,會直接跳轉到 shiroFilterFactoryBean.setLoginUrl(); 設置的 url
UserRealm:
要繼承 AuthorizingRealm 類來自定義我們自己的 realm 以進行我們自定義的認證和授權操作
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 獲取授權用戶信息(principalCollection存放了認證成功的用戶信息)
User user = (User) principalCollection.iterator().next();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// TODO 改爲從數據庫中獲取該用戶的角色
authorizationInfo.addRole(user.getRole());
return authorizationInfo;
}
// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
String username = token.getUsername();
String password = String.valueOf(token.getPassword());
// TODO 改爲從數據庫獲取對應用戶名密碼的用戶
User user = userService.getUserByName(username);
if (null == user) {
throw new UnknownAccountException("用戶名不存在");
} else if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("密碼不正確");
}
SecurityUtils.getSubject().getSession().setAttribute("user", user);
return new SimpleAuthenticationInfo(user, password, getName());
}
}
重寫的兩個方法分別是實現身份認證以及授權認證。
- doGetAuthenticationInfo():需要進行認證時,纔會進入此方法。如調用:Subject.login(token)
- doGetAuthorizationInfo():需要授權認證時,纔會進入此方法。如:配置類中配置了 ==filterMap.put("/admin/**", “roles[admin]”);==管理員的角色,這時,如果訪問了以"/admin"開頭的url,則會進入此方法。
UserServiceImpl:
UserServiceImpl是UserService接口的實現類,這裏就貼出它的實現類的代碼了。
@Service
public class UserServiceImpl implements UserService {
private static Map<String, User> userMap = new HashMap<>();
static {
userMap.put("zzc", new User("1", "zzc", "666", "user"));
userMap.put("wzc", new User("2", "wzc", "888", "user"));
userMap.put("yht", new User("3", "yht", "999", "admin!"));
}
@Override
public User getUserByName(String username) {
// Map通過key進行得到value
return userMap.get(username);
}
}
這裏沒使用數據庫了,就直接使用了假數據,並存儲在了Map中,通過靜態代碼塊添加了部分數據。Map的key是id(這裏假設id是與username 一樣,方便調用getUserByName()),value是User。
User:
@Data
public class User {
private String id;
private String username;
private String password;
// 角色
private String role;
public User(String id, String username, String password, String role) {
this.id = id;
this.username = username;
this.password = password;
this.role = role;
}
}
好了,接下來就是controller層了。
GuestController:
@RestController
@RequestMapping("/guest")
public class GuestController {
@GetMapping("/enter")
public String enter() {
return "歡迎進入,您的身份是遊客";
}
}
UserController:
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/enter")
public String enter() {
return "歡迎進入,您的身份是用戶";
}
}
AdminController:
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/enter")
public String enter() {
return "Admin enter!!";
}
}
這上面Controller裏面的內容基本一致,就是爲了測試不同的url是由不同的角色才能訪問的(接下去繼續看)。
LoginController:
@Slf4j
@Controller
public class LoginController {
@Autowired
private HttpServletRequest request;
// 進入登錄頁面
@GetMapping("/login")
public String login() {
log.info("進入login()方法");
return "login";
}
// 執行登錄操作
@PostMapping("/doLogin")
public String doLogin(String username, String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
String errorMsg = null;
try {
SecurityUtils.getSubject().login(token);
} catch (AuthenticationException e) {
if (e instanceof UnknownAccountException) {
errorMsg = "用戶不存在";
} else if (e instanceof LockedAccountException) {
errorMsg = "用戶被禁用";
} else if (e instanceof IncorrectCredentialsException) {
errorMsg = "密碼錯誤";
} else {
errorMsg = "認證失敗";
}
request.setAttribute("errorMsg", errorMsg);
return "/login";
}
return "redirect:/success";
}
// 登錄成功頁面
@GetMapping("/success")
public String success() {
return "success";
}
// 退出
@GetMapping("/logout")
public String logout() {
log.info("進入logout()方法");
SecurityUtils.getSubject().logout();
return "redirect:/login";
}
// 授權失敗時,跳轉到role頁面
@GetMapping("/role")
public String role() {
return "role";
}
}
login.ftl:
登錄頁面
<body>
<h1>用戶登錄</h1>
<form action="/doLogin" method="post">
username: <input type="text" name="username" /><br>
password: <input type="password" name="password" /><br>
<input type="submit" value="登錄" /><br>
</form>
<div style="color: red">${errorMsg}</div>
</body>
success.ftl:
<body>
<h1>登錄成功:${user.username}</h1>
<div>
<a href="/logout">退出</a><br>
<a href="/admin/enter">進入管理員頁面</a>
</div>
</body>
role.ftl:
<body>
<h1>權限不足</h1>
</body>
5)、測試
針對之前配置類的攔截器中的配置進行測試:
- 以"/guest"開頭的url,匿名訪問(不需要認證/登錄)
filterMap.put("/guest/**", "anon");
訪問:http://localhost:8080/guest/enter 的url,由於不需要認證,所以可以直接訪問。
- 以"/user"開頭的url,需要"user"的角色
filterMap.put("/user/**", "roles[user]");
上述的url還沒有按回車鍵,按後:
跳轉到了這個登錄界面(按回車鍵,跳轉到登錄界面,這個過程,我無法證明,但讀者可以去試試。)。話說回來,爲什麼會跳轉到登錄界面?因爲訪問以"/user"開頭的url是需要"user"的角色的,都還沒有登錄,怎麼可能會讓你訪問呢?所以,需要你去登錄。
執行登錄邏輯:
進行登錄操作時,會調用LoginController.doLogin()方法,會執行
SecurityUtils.getSubject().login(token);
上面那條語句,所以,會調用UserRealm.doGetAuthenticationInfo()方法進行認證(如果忘記了,請往上看UserRealm類那塊)。
我們以“zzc-666”的用戶進行登錄,它的角色是"user"。
先使用錯的賬號進行登錄:
再來使用一個錯的賬號:
使用對的賬號和密碼:
在登錄成功界面,我們來訪問“進入管理員頁面的”鏈接,它的html代碼爲:
<a href="/admin/enter">進入管理員頁面</a>
根據配置類中的配置:
filterMap.put("/admin/**", "roles[admin]");
可知,訪問此條鏈接是需要管理員角色的,那麼我這個"user"角色能否訪問?
由此可知,權限不足,無法訪問,那麼爲何會跳轉到這個頁面呢?
根據此配置:
shiroFilterFactoryBean.setUnauthorizedUrl("/role");
好了,SpringBoot集成Shiro的簡單介紹就到這兒了。
【參考資料】
教你 Shiro 整合 SpringBoot,避開各種坑