SpringBoot--- Shiro(攔截,認證)、Thymeleaf(模板引擎)

凌丹妙藥發表於2020-08-21

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 語法的名稱空間)需要素材請聯絡我。

image-20200821143141470

配置類

自定義 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;
    }

暫時不配置登入頁面,這是下一個章節的內容,啟動訪問,並點選訪問需要登入的資源。

image-20200821150120203

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 為我們將這些聯絡了起來。

image-20200821162537541

生成令牌,我們回到 UserRealm 類 的 doGetAuthenticationInfo 方法。

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(
        AuthenticationToken authenticationToken) throws AuthenticationException {

有一個引數 AuthenticationToken ,它的型別是 認證令牌,檢視原始碼,發現是一個介面。

那就來看他的實現類。

image-20200821164706610

最後一個是使用者名稱密碼令牌,檢視原始碼,它的構造方法,可以用來生成一個用於身份認證的令牌。

/**
     * 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 將會由之前登入方法捕獲到異常,再做相應處理。

密碼不由我們進行處理,可以說是非常安全了。

注意返回值,需要的只是一個介面,我們可以返回它的實現類。

image-20200821171303209

圖中兩個都是可以使用的,都可以用於認證。

它的建構函式,檢視原始碼

 /**
     * 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 ,可以通俗地理解為 使用者名稱,密碼,最常見的組合也是使用者名稱和密碼。

測試

image-20200821170721145

image-20200821170734936


如有疑問,還可以檢視官網給出的十分鐘快速入門。https://shiro.apache.org/10-minute-tutorial.html

相關文章