天天看點

Day374.shiro授權&Shiro+jsp整合Springboot -Shiro

Shiro授權

一、授權

授權

,即通路控制,控制誰能通路哪些資源。主體進行身份認證後需要配置設定權限方可通路系統的資源,對于某些資源沒有權限是無法通路的。

二、關鍵對象

授權可簡單了解為who對what(which)進行How操作:

Who,即主體(Subject)

,主體需要通路系統中的資源。

What,即資源(Resource)

,如系統菜單、頁面、按鈕、類方法、系統商品資訊等。資源包括

資源類型

資源執行個體

,比如

商品資訊為資源類型

,類型為t01的商品為

資源執行個體

,編号為001的商品資訊也屬于資源執行個體。

How,權限/許可(Permission)

,規定了主體對資源的操作許可,權限離開資源沒有意義,如使用者查詢權限、使用者添權重限、某個類方法的調用權限、編号為001使用者的修改權限等,通過權限可知主體對哪些資源都有哪些操作許可。

三、授權流程

Day374.shiro授權&Shiro+jsp整合Springboot -Shiro

四、授權方式

  • 基于角色的通路控制
    • RBAC基于角色的通路控制(Role-Based Access Control)是以角色為中心進行通路控制
      if(subject.hasRole("admin")){
         //操作什麼資源
      }
                 
  • 基于資源的通路控制
    • RBAC基于資源的通路控制(Resource-Based Access Control)是以資源為中心進行通路控制
      if(subject.isPermission("user:update:01")){ //資源執行個體
        //對01使用者進行修改
      }
      if(subject.isPermission("user:update:*")){  //資源類型
        //對01使用者進行修改
      }
                 

五、權限字元串

​ 權限字元串的規則是:資源辨別符:操作:資源執行個體辨別符,意思是對哪個資源的哪個執行個體具有什麼操作,“:”是資源/操作/執行個體的分割符,權限字元串也可以使用*通配符。

例子:

  • 使用者建立權限:user:create,或user:create:*
  • 使用者修改執行個體001的權限:user:update:001
  • 使用者執行個體001的所有權限:user : * :001
  • A:B:C,A通過B來操作C

六、shiro中授權程式設計實作方式

  • 程式設計式
    Subject subject = SecurityUtils.getSubject();
    if(subject.hasRole(“admin”)) {
    	//有權限
    } else {
    	//無權限
    }
               
  • 注解式
    @RequiresRoles("admin")
    public void hello() {
    	//有權限
    }
               
  • 标簽式
    JSP/GSP 标簽:在JSP/GSP 頁面通過相應的标簽完成:
    <shiro:hasRole name="admin">
    	<!— 有權限—>
    </shiro:hasRole>
    注意: Thymeleaf 中使用shiro需要額外內建!
               

七、開發授權

1.realm的實作
public class CustomerRealm extends AuthorizingRealm {
    //認證方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //擷取主身份,也就是使用者名
        String primaryPrincipal = (String) principals.getPrimaryPrincipal();
        System.out.println("primaryPrincipal = " + primaryPrincipal);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //添加角色
        simpleAuthorizationInfo.addRole("admin");
        
        simpleAuthorizationInfo.addStringPermission("user:update:*");
        simpleAuthorizationInfo.addStringPermission("product:*:*");

        return simpleAuthorizationInfo;
    }

    //授權方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String principal = (String) token.getPrincipal();
        if("xiaochen".equals(principal)){
            String password = "3c88b338102c1a343bcb88cd3878758e";
            String salt = "Q4F%";
            return new SimpleAuthenticationInfo(principal,
                                                password, 
                                               ByteSource.Util.bytes(salt),
                                                this.getName());
        }
        return null;
    }

}
           
2.授權
public class TestAuthenticatorCusttomerRealm {
    public static void main(String[] args) {
        //建立securityManager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //IniRealm realm = new IniRealm("classpath:shiro.ini");
        //設定為自定義realm擷取認證資料
        CustomerRealm customerRealm = new CustomerRealm();
        //設定md5加密
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1024);//設定散列次數
        customerRealm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(customerRealm);
        //将安裝工具類中設定預設安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //擷取主體對象
        Subject subject = SecurityUtils.getSubject();
        //建立token令牌
        UsernamePasswordToken token = new UsernamePasswordToken("achang", "123");
        try {
            subject.login(token);//使用者登入
            System.out.println("登入成功~~");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("使用者名錯誤!!");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密碼錯誤!!!");
        }
        
        //認證通過
        if(subject.isAuthenticated()){
            //基于角色權限管理
            boolean admin = subject.hasRole("admin");
            
            //基于多角色權限控制,hasAllRoles隻要有一個該subject不含有,就傳回false
            boolean roles = subject.hasAllRoles(Arrays.asList("admin","super"));
            
            //是否具有其中一個角色,傳回布爾數組,含有就是t,不含有就是f
            boolean[] booleans = subject.hasRoles(Arrays.asList("admin","super","user"));
			
            
            boolean permitted = subject.isPermitted("product:create:001");
            System.out.println(permitted);
        }

    }
}
           

整合SpringBoot項目實戰

零、整合思路

Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro

一、建立springboot項目

Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro

二、引入shiro依賴

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring-boot-starter</artifactId>
  <version>1.5.3</version>
</dependency>
           

三、配置shiro環境

Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
1.配置shiroFilterFactoryBean
//1、shiroFilter
//負責攔截所有請求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    //注入安全管理器
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    //設定受限資源
    HashMap<String, String> map = new HashMap<>();
    map.put("/index.jsp","authc");//authc 請求這個資源,需要認證授權
    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

    //預設認證界面路徑
    shiroFilterFactoryBean.setLoginUrl("/login.jsp");

    return shiroFilterFactoryBean;
}
           
2.配置WebSecurityManager
//2、安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
    DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
    defaultWebSecurityManager.setRealm(realm);
    return defaultWebSecurityManager;
}
           
3.建立自定義realm
//自定義Realm
public class CustomRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        return null;
    }
}
           
4.配置自定義realm
//3、自定義Realm
@Bean
@Primary
public Realm getRealm(){
    return new CustomRealm();
}
           
5.編寫控制器跳轉至index.html
@Controller
public class IndexController {
    @RequestMapping("index")
    public String index(){
        System.out.println("跳轉至首頁");
        return "index";
    }
}
           
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
6.啟動springboot應用通路index
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
  • 注意:
    • 預設在配置好shiro環境後預設環境中沒有對項目中任何資源進行權限控制,所有現在項目中所有資源都可以通過路徑通路
7.加入權限控制
  • 修改ShiroFilterFactoryBean配置
    //設定受限資源
    HashMap<String, String> map = new HashMap<>();
    map.put("/index.jsp","authc");//authc 請求這個資源,需要認證授權
    //map.put("/**","authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    
    //預設認證界面路徑
    shiroFilterFactoryBean.setLoginUrl("/login.jsp");
               
    • /** 代表攔截項目中一切資源 authc 代表shiro中的一個filter的别名,詳細内容看文檔的shirofilter清單
8.重新開機項目通路檢視
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro

四、常見過濾器

  • 注意: shiro提供和多個預設的過濾器,我們可以用這些過濾器來配置控制指定url的權限:
配置縮寫 對應的過濾器 功能
anon AnonymousFilter 指定url可以匿名通路
authc FormAuthenticationFilter 指定url需要form表單登入,預設會從請求中擷取

username

password

,

rememberMe

等參數并嘗試登入,如果登入不了就會跳轉到loginUrl配置的路徑。我們也可以用這個過濾器做預設的登入邏輯,但是一般都是我們自己在控制器寫登入邏輯的,自己寫的話出錯傳回的資訊都可以定制嘛。
authcBasic BasicHttpAuthenticationFilter 指定url需要basic登入
logout LogoutFilter 登出過濾器,配置指定url就可以實作退出功能,非常友善
noSessionCreation NoSessionCreationFilter 禁止建立會話
perms PermissionsAuthorizationFilter 需要指定權限才能通路
port PortFilter 需要指定端口才能通路
rest HttpMethodPermissionFilter 将http請求方法轉化成相應的動詞來構造一個權限字元串,這個感覺意義不大,有興趣自己看源碼的注釋
roles RolesAuthorizationFilter 需要指定角色才能通路
ssl SslFilter 需要https請求才能通路
user UserFilter 需要已登入或“記住我”的使用者才能通路

五、認證實作

1. 在login.jsp中開發認證界面
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
<form action="${pageContext.request.contextPath}/user/login" method="post">
  使用者名:<input type="text" name="username" > <br/>
  密碼  : <input type="text" name="password"> <br>
  <input type="submit" value="登入">
</form>
           
2. 開發controller
@Controller
@RequestMapping("user")
public class UserController {
    /**
    * 用來處理身份認證
    * @param username
    * @param password
    * @return
    */
    @RequestMapping("login")
    public String login(String username,String password){
        //擷取主體對象
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username,password));
            return  "redirect:/index.jsp";
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("使用者名錯誤!");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密碼錯誤!");
        }
        return "redirect:/login.jsp";
    }
}
           
  • 在認證過程中使用subject.login進行認證
3.開發realm中傳回靜态資料(未連接配接資料庫)
@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("==========================");
        String principal = (String) token.getPrincipal();
        if("achang".equals(principal)){
            return new SimpleAuthenticationInfo(principal,"123456",this.getName());
        }
        return null;
    }
}
           
4.啟動項目以realm中定義靜态資料進行認證
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
  • 認證功能沒有md5和随機鹽的認證就實作啦

六、退出認證

1.開發頁面退出連接配接
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
2.開發controller
@Controller
@RequestMapping("user")
public class UserController {
  /**
    * 登出
    *
    */
  @RequestMapping("logout")
  public String logout(){
    Subject subject = SecurityUtils.getSubject();
    subject.logout();//退出使用者
    return "redirect:/login.jsp";
  }
}
           
3.修改退出連接配接通路退出路徑
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
4.退出之後通路受限資源立即傳回認證界面
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro

七、MD5、Salt的認證實作

1.開發資料庫注冊

0.開發注冊界面
<h1>使用者注冊</h1>
<form action="${pageContext.request.contextPath}/user/register" method="post">
  使用者名:<input type="text" name="username" > <br/>
  密碼  : <input type="text" name="password"> <br>
  <input type="submit" value="立即注冊">
</form>
           
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
1.建立資料表結構
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
    `id` int(6) NOT NULL AUTO_INCREMENT,
    `username` varchar(40) DEFAULT NULL,
    `password` varchar(40) DEFAULT NULL,
    `salt` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

SET FOREIGN_KEY_CHECKS = 1;
           
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
2.項目引入依賴
<!--mybatis相關依賴-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.38</version>
</dependency>

<!--druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.19</version>
</dependency>
           
3.配置application.properties配置檔案
server.port=8888
server.servlet.context-path=/shiro
spring.application.name=shiro

spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
#新增配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root


mybatis.type-aliases-package=com.achang.springboot_jsp_shiro.entity
mybatis.mapper-locations=classpath:com/achang/mapper/*.xml

           
4.建立entity
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String  id;
    private String username;
    private String password;
    private String salt;
}
           
5.建立DAO接口
@Mapper
public interface UserDAO {
    void save(User user);
}
           
6.開發mapper配置檔案
<insert id="save" parameterType="User" useGeneratedKeys="true" keyProperty="id">
  insert into t_user values(#{id},#{username},#{password},#{salt})
</insert>
           
7.開發service接口
public interface UserService {
    //注冊使用者方法
    void register(User user);
}
           
8.建立salt工具類
public class SaltUtils {
    /**
     * 生成salt的靜态方法
     * @param n
     * @return
     */
    public static String getSalt(int n){
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char aChar = chars[new Random().nextInt(chars.length)];
            sb.append(aChar);
        }
        return sb.toString();
    }
}
           
9.開發service實作類
@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDAO userDAO;

    @Override
    public void register(User user) {
        //處理業務調用dao
        //1.生成随機鹽
        String salt = SaltUtils.getSalt(8);
        //2.将随機鹽儲存到資料
        user.setSalt(salt);
        //3.明文密碼進行md5 + salt + hash散列
        Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
        user.setPassword(md5Hash.toHex());
        userDAO.save(user);
    }
}
           
10.開發Controller
@Controller
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 使用者注冊
     */
    @RequestMapping("register")
    public String register(User user) {
        try {
            userService.register(user);
            return "redirect:/login.jsp";
        }catch (Exception e){
            e.printStackTrace();
            return "redirect:/register.jsp";
        }
    }
}
           
11.啟動項目進行注冊
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro

2.開發資料庫認證

0.開發DAO
@Mapper
public interface UserDAO {

    void save(User user);
		//根據身份資訊認證的方法
    User findByUserName(String username);
}
           
1.開發mapper配置檔案
<select id="findByUserName" parameterType="String" resultType="User">
  select id,username,password,salt from t_user
  where username = #{username}
</select>
           
2.開發Service接口
public interface UserService {
    //注冊使用者方法
    void register(User user);
    //根據使用者名查詢業務的方法
    User findByUserName(String username);
}
           
3.開發Service實作類
@Service("userService")
@Transactional
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDAO userDAO;
    @Override
    public User findByUserName(String username) {
        return userDAO.findByUserName(username);
    }
}
           
4.開發在工廠中擷取bean對象的工具類
@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }


    //根據bean名字擷取工廠中指定bean 對象
    public static Object getBean(String beanName){
        return context.getBean(beanName);
    }
}
           
5.修改自定義realm
@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("==========================");

        //根據身份資訊
        String principal = (String) token.getPrincipal();
        //在工廠中擷取service對象
        UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
				//根據身份資訊查詢
        User user = userService.findByUserName(principal);

        if(!ObjectUtils.isEmpty(user)){
            //傳回資料庫資訊
            return new SimpleAuthenticationInfo(user.getUsername(),
                                                user.getPassword(),
                                                ByteSource.Util.bytes(user.getSalt()),
                                                this.getName());
        }
        return null;
    }
           
6.修改ShiroConfig中realm使用憑證比對器以及hash散列
@Bean
public Realm getRealm(){
  CustomerRealm customerRealm = new CustomerRealm();
  //設定hashed憑證比對器
  HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
  //設定md5加密
  credentialsMatcher.setHashAlgorithmName("md5");
  //設定散列次數
  credentialsMatcher.setHashIterations(1024);
  customerRealm.setCredentialsMatcher(credentialsMatcher);
  return customerRealm;
}
           
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro

八、授權實作

0.頁面資源授權
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

<shiro:hasAnyRoles name="user,admin">
        <li><a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >使用者管理</a>
            <ul>
                <shiro:hasPermission name="user:add:*">
                <li><a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >添加</a></li>
                </shiro:hasPermission>
                <shiro:hasPermission name="user:delete:*">
                    <li><a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >删除</a></li>
                </shiro:hasPermission>
                <shiro:hasPermission name="user:update:*">
                    <li><a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >修改</a></li>
                </shiro:hasPermission>
                <shiro:hasPermission name="user:find:*">
                    <li><a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >查詢</a></li>
                </shiro:hasPermission>
            </ul>
        </li>
        </shiro:hasAnyRoles>
        <shiro:hasRole name="admin">
            <li><a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >商品管理</a></li>
            <li><a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >訂單管理</a></li>
            <li><a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >物流管理</a></li>
        </shiro:hasRole>
           
1.代碼方式授權
@RequestMapping("save")
public String save(){
  System.out.println("進入方法");
  //擷取主體對象
  Subject subject = SecurityUtils.getSubject();
  //代碼方式
  if (subject.hasRole("admin")) {
    System.out.println("儲存訂單!");
  }else{
    System.out.println("無權通路!");
  }
  //基于權限字元串
  //....
  return "redirect:/index.jsp";
}
           
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
2.方法調用授權
  • @RequiresRoles 用來基于角色進行授權
  • @RequiresPermissions 用來基于權限進行授權
@RequiresRoles(value={"admin","user"})//用來判斷角色  同時具有 admin user
@RequiresPermissions("user:update:01") //用來判斷權限字元串
@RequestMapping("save")
public String save(){
  System.out.println("進入方法");
  return "redirect:/index.jsp";
}
           
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
3.授權資料持久化
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_pers
-- ----------------------------
DROP TABLE IF EXISTS `t_pers`;
CREATE TABLE `t_pers` (
    `id` int(6) NOT NULL AUTO_INCREMENT,
    `name` varchar(80) DEFAULT NULL,
    `url` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
    `id` int(6) NOT NULL AUTO_INCREMENT,
    `name` varchar(60) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
    `id` int(6) NOT NULL,
    `roleid` int(6) DEFAULT NULL,
    `permsid` int(6) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
    `id` int(6) NOT NULL AUTO_INCREMENT,
    `username` varchar(40) DEFAULT NULL,
    `password` varchar(40) DEFAULT NULL,
    `salt` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
    `id` int(6) NOT NULL,
    `userid` int(6) DEFAULT NULL,
    `roleid` int(6) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

SET FOREIGN_KEY_CHECKS = 1;

           
4.建立dao方法
//根據使用者名查詢所有角色
User findRolesByUserName(String username);
//根據角色id查詢權限集合
List<Perms> findPermsByRoleId(String id);
           
5.mapper實作
<resultMap id="userMap" type="User">
      <id column="uid" property="id"/>
      <result column="username" property="username"/>
      <!--角色資訊-->
      <collection property="roles" javaType="list" ofType="Role">
        <id column="id" property="id"/>
        <result column="rname" property="name"/>
      </collection>
    </resultMap>

    <select id="findRolesByUserName" parameterType="String" resultMap="userMap">
      SELECT u.id uid,u.username,r.id,r.NAME rname
      FROM t_user u
      LEFT JOIN t_user_role ur
      ON u.id=ur.userid
      LEFT JOIN t_role r
      ON ur.roleid=r.id
      WHERE u.username=#{username}
    </select>

    <select id="findPermsByRoleId" parameterType="String" resultType="Perms">
      SELECT p.id,p.NAME,p.url,r.NAME
      FROM t_role r
      LEFT JOIN t_role_perms rp
      ON r.id=rp.roleid
      LEFT JOIN t_perms p ON rp.permsid=p.id
      WHERE r.id=#{id}
    </select>
           
6.Service接口
//根據使用者名查詢所有角色
User findRolesByUserName(String username);
//根據角色id查詢權限集合
List<Perms> findPermsByRoleId(String id);
           
7.Service實作
@Override
public List<Perms> findPermsByRoleId(String id) {
  return userDAO.findPermsByRoleId(id);
}

@Override
public User findRolesByUserName(String username) {
  return userDAO.findRolesByUserName(username);
}
           
8.修改自定義realm
public class CustomerRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //擷取身份資訊
        String primaryPrincipal = (String) principals.getPrimaryPrincipal();
        System.out.println("調用授權驗證: "+primaryPrincipal);
        //根據主身份資訊擷取角色 和 權限資訊
        UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
        User user = userService.findRolesByUserName(primaryPrincipal);
        //授權角色資訊
        if(!CollectionUtils.isEmpty(user.getRoles())){
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            user.getRoles().forEach(role->{
                simpleAuthorizationInfo.addRole(role.getName());
                //權限資訊
                List<Perms> perms = userService.findPermsByRoleId(role.getId());
                if(!CollectionUtils.isEmpty(perms)){
                    perms.forEach(perm->{
                        simpleAuthorizationInfo.addStringPermission(perm.getName());
                    });
                }
            });
            return simpleAuthorizationInfo;
        }
        return null;
    }
}
           
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
9.啟動測試

九、使用CacheManager

1.Cache 作用

  • Cache 緩存: 計算機記憶體中一段資料
  • 作用: 用來減輕DB的通路壓力,進而提高系統的查詢效率
  • 流程:
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro

2.使用shiro中預設EhCache實作緩存

本地緩存

①引入依賴
<!--引入shiro和ehcache-->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-ehcache</artifactId>
  <!--注意版本,要與shiro一緻-->
  <version>1.5.3</version>
</dependency>
           
②開啟緩存
//3.建立自定義realm
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        //修改憑證校驗比對器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //設定加密算法為md5
        credentialsMatcher.setHashAlgorithmName("MD5");
        //設定散列次數
        credentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(credentialsMatcher);

        //開啟緩存管理器
        customerRealm.setCachingEnabled(true);//開啟全局緩存
        customerRealm.setAuthenticationCacheingEnabled(true);//開啟認證緩存
        customerRealm.setAuthorizationCachingEnabled(true);//開啟授權緩存
        customerRealm.setCacheManager(new EhCacheManager());
        
        return customerRealm;
    }
           
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
③啟動重新整理頁面進行測試
  • 注意:如果控制台沒有任何sql展示說明緩存已經開啟

3.shiro中使用Redis作為緩存實作

①引入redis依賴
<!--redis整合springboot-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
           
②配置redis連接配接
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0
           
3.啟動redis服務
➜  bin ls
dump.rdb        redis-check-aof redis-cli       redis-server    redis.conf
redis-benchmark redis-check-rdb redis-sentinel  redis-trib.rb
➜  bin ./redis-server redis.conf
           
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
4.開發RedisCacheManager
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
public class RedisCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        System.out.println("緩存名稱: "+cacheName);
        return new RedisCache<K,V>(cacheName);
    }
}
           
5.開RedisCache實作
public class RedisCache<K,V> implements Cache<K,V> {

    private String cacheName;

    public RedisCache() {
    }

    public RedisCache(String cacheName) {
        this.cacheName = cacheName;
    }

    @Override
    public V get(K k) throws CacheException {
        System.out.println("擷取緩存:"+ k);
        return (V) getRedisTemplate().opsForHash().get(this.cacheName,k.toString());
    }

    @Override
    public V put(K k, V v) throws CacheException {
        System.out.println("設定緩存key: "+k+" value:"+v);
        getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
        return null;
    }

    @Override
    public V remove(K k) throws CacheException {
        return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
    }

    @Override
    public v remove(k k) throws CacheException {
        return (v) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
    }

    @Override
    public void clear() throws CacheException {
        getRedisTemplate().delete(this.cacheName);
    }

    @Override
    public int size() {
        return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
    }

    @Override
    public Set<k> keys() {
        return getRedisTemplate().opsForHash().keys(this.cacheName);
    }

    @Override
    public Collection<v> values() {
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }

    private RedisTemplate getRedisTemplate(){
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }


    //封裝擷取redisTemplate
    private RedisTemplate getRedisTemplate(){
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}
           
6.啟動項目測試發現報錯
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
  • 錯誤解釋: 由于shiro中提供的simpleByteSource實作沒有實作序列化,所有在認證時出現錯誤資訊
  • 解決方案: 需要自動salt實作序列化
    • 自定義salt實作序列化
      //自定義salt實作  實作序列化接口
      public class MyByteSource extends SimpleByteSource implements Serializable {
          public MyByteSource(String string) {
              super(string);
          }
      }
                 
    • 在realm中使用自定義salt
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("==========================");
        //根據身份資訊
        String principal = (String) token.getPrincipal();
        //在工廠中擷取service對象
        UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
        User user = userService.findByUserName(principal);
        if(!ObjectUtils.isEmpty(user)){
          return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), 
                                            new MyByteSource(user.getSalt()),this.getName());
        }
        return null;
      }
                 
      Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
7.再次啟動測試,發現可以成功放入redis緩存
Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro

4. 加入驗證碼驗證

0.開發頁面加入驗證碼
  • 開發控制器
    @RequestMapping("getImage")
    public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
      //生成驗證碼
      String code = VerifyCodeUtils.generateVerifyCode(4);
      //驗證碼放入session
      session.setAttribute("code",code);
      //驗證碼存入圖檔
      ServletOutputStream os = response.getOutputStream();
      response.setContentType("image/png");
      VerifyCodeUtils.outputImage(220,60,os,code);
    }
               
  • 放行驗證碼
    Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
  • 開發頁面
    Day374.shiro授權&amp;Shiro+jsp整合Springboot -Shiro
  • 修改認證流程
    @RequestMapping("login")
        public String login(String username, String password,String code,HttpSession session) {
            //比較驗證碼
            String codes = (String) session.getAttribute("code");
            try {
                if (codes.equalsIgnoreCase(code)){
                    //擷取主體對象
                    Subject subject = SecurityUtils.getSubject();
                        subject.login(new UsernamePasswordToken(username, password));
                        return "redirect:/index.jsp";
                }else{
                    throw new RuntimeException("驗證碼錯誤!");
                }
            } catch (UnknownAccountException e) {
                e.printStackTrace();
                System.out.println("使用者名錯誤!");
            } catch (IncorrectCredentialsException e) {
                e.printStackTrace();
                System.out.println("密碼錯誤!");
            }catch (Exception e){
                e.printStackTrace();
                System.out.println(e.getMessage());
            }
            return "redirect:/login.jsp";
        }
               
  • 修改salt不能序列化的問題
    //自定義salt實作  實作序列化接口
    public class MyByteSource implements ByteSource,Serializable {
    
        private  byte[] bytes;
        private String cachedHex;
        private String cachedBase64;
    
        //加入無參數構造方法實作序列化和反序列化
        public MyByteSource(){
    
        }
    
        public MyByteSource(byte[] bytes) {
            this.bytes = bytes;
        }
    
        public MyByteSource(char[] chars) {
            this.bytes = CodecSupport.toBytes(chars);
        }
    
        public MyByteSource(String string) {
            this.bytes = CodecSupport.toBytes(string);
        }
    
        public MyByteSource(ByteSource source) {
            this.bytes = source.getBytes();
        }
    
        public MyByteSource(File file) {
            this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
        }
    
        public MyByteSource(InputStream stream) {
            this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
        }
    
        public static boolean isCompatible(Object o) {
            return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
        }
    
        public byte[] getBytes() {
            return this.bytes;
        }
    
        public boolean isEmpty() {
            return this.bytes == null || this.bytes.length == 0;
        }
    
        public String toHex() {
            if (this.cachedHex == null) {
                this.cachedHex = Hex.encodeToString(this.getBytes());
            }
    
            return this.cachedHex;
        }
    
        public String toBase64() {
            if (this.cachedBase64 == null) {
                this.cachedBase64 = Base64.encodeToString(this.getBytes());
            }
    
            return this.cachedBase64;
        }
    
        public String toString() {
            return this.toBase64();
        }
    
        public int hashCode() {
            return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
        }
    
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            } else if (o instanceof ByteSource) {
                ByteSource bs = (ByteSource)o;
                return Arrays.equals(this.getBytes(), bs.getBytes());
            } else {
                return false;
            }
        }
    
        private static final class BytesHelper extends CodecSupport {
            private BytesHelper() {
            }
    
            public byte[] getBytes(File file) {
                return this.toBytes(file);
            }
    
            public byte[] getBytes(InputStream stream) {
                return this.toBytes(stream);
            }
        }
    }