SpringBoot--- Shiro(攔截,認證)、Thymeleaf(模板引擎)
環境
IDEA :2020.1
SpringBoot: 2.3.3
Java : 8
版本依賴:
shiro-spring : 1.6.0
準備 :環境搭建
匯入依賴
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.6.0</version>
</dependency>
<!-- thymeleaf-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
spring與Shiro 的整合包,模板引擎用的是 Thymeleaf。
編寫 Controller 類
@Controller
public class HelloController {
@RequestMapping({"/","/index"})
public String index(Model model){
model.addAttribute("msg","Hello Shiro");
return "welcome";
}
@RequestMapping("/level1/{id}")
public String toLevel1(@PathVariable("id") int id){
return "pages/level1/" + id;
}
@RequestMapping("/level2/{id}")
public String toLevel2(@PathVariable("id") int id){
return "pages/level2/" + id;
}
@RequestMapping("/level3/{id}")
public String toLevel3(@PathVariable("id") int id){
return "pages/level3/" + id;
}
}
頁面素材
採用之前整合 SpringSecurity 時候使用的HTML 頁面。(提醒一下,注意匯入支援 Thymeleaf 語法的名稱空間)需要素材請聯絡我。
配置類
自定義 Realm,用於認證,授權(未完整)。
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行了授權===>doGetAuthorizationInfo");
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("執行了認證===>doGetAuthoricationInfo");
return null;
}
}
Shiro 配置類(基本框架)
@Configuration
public class ShiroConfig {
//3、ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getfilterFactoryBean(
@Qualifier("securityManager") DefaultWebSecurityManager manager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//配置SecurityManager
bean.setSecurityManager(manager);
return bean;
}
//2、DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager getsecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//關聯 UserRealm
manager.setRealm(userRealm);
return manager;
}
//1、建立 realm 類,需要建立 UserRealm 繼承 AuthorizingRealm 並重寫方法
@Bean(name = "userRealm")
public UserRealm realm(){
return new UserRealm();
}
}
啟動測試,程式是否正常。
1、攔截
攔截本質是通過過濾器,攔截器實現的。
Shiro 採用 ShiroFilterFactoryBean 配置 一個HashMap 來實現。在配置之前我們需要了解配置的含義。
許可權過濾器及配置釋義
anon:例子/admins/*=anon 沒有引數,表示可以匿名使用。*
authc:例如/admins/user/*=authc表示*需要認證(登入)*才能使用,沒有引數*
roles(角色):例子/admins/user/=roles[admin],引數可以寫多個,多個時必須加上引號,並且引數之間用逗號分割,當有多個引數時,例如admins/user/=roles["admin,guest"],每個引數通過才算通過,相當於hasAllRoles()方法。
perms(許可權):例子/admins/user/=perms[user:add:*],引數可以寫多個,多個時必須加上引號,並且引數之間用逗號分割,例如/admins/user/=perms["user:add:,user:modify:"],當有多個引數時必須每個引數都通過才通過,想當於isPermitedAll()方法。
rest:例子/admins/user/=rest[user],根據請求的方法,相當於/admins/user/=perms[user:method] ,其中method為post,get,delete等。
port:例子/admins/user/**=port[8081],當請求的url的埠不是8081是跳轉到schemal://serverName:8081?queryString,其中schmal是協議http或https等,serverName是你訪問的host,8081是url配置裡port的埠,queryString
是你訪問的url裡的?後面的引數。
authcBasic:例如/admins/user/**=authcBasic沒有參數列示httpBasic認證
ssl:例子/admins/user/**=ssl沒有引數,表示安全的url請求,協議為https
user:例如/admins/user/**=user沒有參數列示必須存在使用者(使用了cookies session儲存了使用者),當登入操作時不做檢查
配置過濾並測試
在 ShiroFilterFactoryBean 的 setFilterChainDefinitionMap 配置過濾規則。同時配置登入頁面,未登入訪問相關 authc 級別頁面將跳轉到登入頁面。
//3、ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getfilterFactoryBean(
@Qualifier("securityManager") DefaultWebSecurityManager manager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//配置SecurityManager
bean.setSecurityManager(manager);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/level*/**","authc");
//配置過濾規則
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
暫時不配置登入頁面,這是下一個章節的內容,啟動訪問,並點選訪問需要登入的資源。
Shiro 竟然還 “貼心” 地準備用JSP ,別說了,看見 JSP就頭疼,學校裡還教了這個.....(這裡多說一句,別死磕JSP了,後端的學學JSTL表示式就行。)
2、認證
Shiro 配置類
結合前面的配置,在 Shiro 配置類 的 ShiroFilterFactoryBean 配置登入的url
//配置登入url
bean.setLoginUrl("/log/toLogin");
配置 Controller 類
@Controller
@RequestMapping("/log")
public class LogController {
@RequestMapping("/toLogin")
public String toLogin(){
return "/pages/login";
}
@RequestMapping("/doLogin")
public String login(String username , String password , Model model){
//獲取當前使用者
Subject subject = SecurityUtils.getSubject();
//生成令牌
UsernamePasswordToken token = new UsernamePasswordToken(username , password);
try{
//用令牌嘗試登入,失敗會丟擲異常
subject.login(token);
//成功則返回主頁
return "welcome";
}catch ( UnknownAccountException e){ //捕捉丟擲使用者名稱錯誤
model.addAttribute("msg","使用者名稱錯誤");
return "/pages/login";
}catch (IncorrectCredentialsException e){//捕捉丟擲密碼錯誤
model.addAttribute("msg","密碼錯誤");
return "/pages/login";
}
}
}
這裡說一下, url 為 /doLogin 的方法,這裡用到了一個 Shiro 的核心物件 Subject ,表示當前使用者,後面將用於登入。
這裡非常神奇,不需要任何傳參,直接通過 SecurityUtils.getSubject() 獲取 Subject 。這一點有點難以理解。不過確實是可以獲取的,可以理解為是 Shiro 為我們將這些聯絡了起來。
生成令牌,我們回到 UserRealm 類 的 doGetAuthenticationInfo 方法。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
有一個引數 AuthenticationToken ,它的型別是 認證令牌,檢視原始碼,發現是一個介面。
那就來看他的實現類。
最後一個是使用者名稱密碼令牌,檢視原始碼,它的構造方法,可以用來生成一個用於身份認證的令牌。
/**
* Constructs a new UsernamePasswordToken encapsulating the username and password submitted
* during an authentication attempt, with a <tt>null</tt> {@link #getHost() host} and a
* <tt>rememberMe</tt> default of <tt>false</tt>.
*
* @param username the username submitted for authentication
* @param password the password character array submitted for authentication
*/
public UsernamePasswordToken(final String username, final char[] password) {
this(username, password, false, null);
}
回到我們的登入方法,我們可以用頁面傳遞過來的username ,password 生成一個 UsernamePasswordToken 令牌。
之後就是用令牌登入,失敗就捕獲一些異常,返回一些提示資訊到頁面。
這裡還剩下一個問題,我們作為開發者怎麼讓 Shiro 知道正確的使用者名稱和密碼,這裡回到了 UserRealm 類 的 doGetAuthenticationInfo 方法進行配置,這裡採用明文使用者名稱和密碼配置,後面整合 Mybatis 將結合資料庫獲取。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("執行了認證===>doGetAuthoricationInfo");
//明文配置使用者名稱和密碼
String username = "tom001";
String password = "1234";
//獲取令牌
UsernamePasswordToken userToken =(UsernamePasswordToken) authenticationToken;
//驗證使用者名稱
if (!userToken.getUsername().equals(username)){
return null;
}
//驗證密碼交由 Shiro 完成
return new SimpleAuthenticationInfo(username,password,"");
}
這裡的獲取令牌,與之前的生成令牌呼應了。具體物件傳遞是由 Shiro 完成的。
驗證使用者名稱不通過,返回 null 將會由之前登入方法捕獲到異常,再做相應處理。
密碼不由我們進行處理,可以說是非常安全了。
注意返回值,需要的只是一個介面,我們可以返回它的實現類。
圖中兩個都是可以使用的,都可以用於認證。
它的建構函式,檢視原始碼
/**
* Constructor that takes in a single 'primary' principal of the account and its corresponding credentials,
* associated with the specified realm.
* <p/>
* This is a convenience constructor and will construct a {@link PrincipalCollection PrincipalCollection} based
* on the {@code principal} and {@code realmName} argument.
*
* @param principal the 'primary' principal associated with the specified realm.
* @param credentials the credentials that verify the given principal.
* @param realmName the realm from where the principal and credentials were acquired.
*/
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName);
this.credentials = credentials;
}
其中,principal,credentials ,可以通俗地理解為 使用者名稱,密碼,最常見的組合也是使用者名稱和密碼。
測試
如有疑問,還可以檢視官網給出的十分鐘快速入門。https://shiro.apache.org/10-minute-tutorial.html