SpringBoot学习笔记(三)—— Shiro

Shiro

此部分内容参考【狂神说 Java】SpringBoot 最新教程 IDEA 版通俗易懂[1]以及学习笔记[2],仅供学习使用!

1、Shiro 简介

Apache Shiro (pronounced “shee-roh”, the Japanese word for ‘castle’) is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management and can be used to secure any application - from the command line applications, mobile applications to the largest web and enterprise applications.

是的,又看到 ==powerful==了,就像Spring Security一样

  • Apache Shiro 是一个 Java 的安全(权限)框架
  • Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
  • Shiro 可以完成,认证授权加密会话管理Web集成缓存等.

2、Shiro 测试环境搭建

新建一个Spring Boot项目spring-boot-08-shiro,添加webthymeleaf框架。

2.1、导入 shiro 依赖

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

2.2、编写 shiro 配置类

在配置类中需要注入三个 Bean:ShiroFilterFactoryBeanDefaultWebSecurityManagerUserRealm

第一步:创建 Realm 对象,需要自定义类继承 AuthorizingRealm,重写认证和授权部分方法。

config.UserRealm.java

package com.thomas.config;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class UserRealm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了===>授权");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了========>认证");
        return null;
    }
}

Realm对象注入到配置类中

config.ShiroConfig

package com.thomas.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
}

第二步:将UserRealm对象设置到defaultWebSecurityManager

@Bean(name = "webSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier(value = "userRealm") UserRealm userRealm) {
    DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();
    webSecurityManager.setRealm(userRealm);
    return webSecurityManager;
}

第三步:将defaultWebSecurityManager设置到ShiroFilterFactoryBean

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier(value = "webSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
    return shiroFilterFactoryBean;
}

2.3、编写 controller

在 controller 层,我们实现简单的页面跳转功能。跳转首页,跳转/usr/update/usr/add

package com.thomas.controller;


import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MyController {
    @GetMapping({"/""/index"})
    public String toIndex(Model model) {
        model.addAttribute("msg""Hello, shiro");
        return "index";
    }

    @GetMapping("/usr/add")
    public String add() {
        return "user/add";
    }

    @GetMapping("/usr/update")
    public String update() {
        return "user/update";
    }
}

2.4、编写前端页面

index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">

    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>首页</h1>
    <p th:text="${msg}"></p>
    <hr>
    <a th:href="@{/usr/add}">add</a> | <a th:href="@{/usr/update}">update</a>
</body>
</html>

user/add.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>add</h1>
</body>
</html>

user/update.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>update</h1>
</body>
</html>

测试,因为没有设置认证,所以均可以访问。

SpringBoot学习笔记(三)—— Shiro
image-20230504083724805

3、Shiro 实现登录拦截

shiro有内置过滤器,是通过DefaultFilter枚举类来实现的,在其中定义了常见的过滤器,我们可以借助这些过滤器来实现拦截请求。

SpringBoot学习笔记(三)—— Shiro

3.1、设置登录验证

我们在ShiroFilterFactoryBean中设置添加 shiro 的内置过滤器,将前面的两个页面设置为authc

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier(value = "webSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    //设置安全管理器
    shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

    //添加shiro的内置过滤器
    /*
        * anon: 无需认证就可以访问
        * authc: 必须认证了才能访问
        * user: 必须拥有 记住我 功能才能用
        * perms: 拥有对某个资源的权限才能访问
        * role: 拥有某个角色权限才能访问
        * */


    Map<String, String> map = new HashMap<>();
    //map.put("/usr/add", "authc");
    //map.put("/usr/update", "authc");
    //支持通配符
    map.put("/usr/*""authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

    return shiroFilterFactoryBean;
}

重新启动项目,测试发现无法访问,此时我们的请求被成功拦截。

SpringBoot学习笔记(三)—— Shiro

3.2、登录页面

当页面需要登录权限而用户未登录时,此时应该跳转到登录页面。

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>登录</h1>
<hr>
<form action="">
  <p>用户名:  <input type="text" name="username"></p>
  <p>密码:  <input type="password" name="password"></p>
  <p><input type="submit"></p>
</form>

跳转请求

package com.thomas.controller;


import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MyController {
    ...
    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login";
    }
}

设置登录页面

package com.thomas.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    //ShiroFilterFactoryBean:第三步
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier(value = "webSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //添加shiro的内置过滤器
        /*
        * anon: 无需认证就可以访问
        * authc: 必须认证了才能访问
        * user: 必须拥有 记住我 功能才能用
        * perms: 拥有对某个资源的权限才能访问
        * role: 拥有某个角色权限才能访问
        * */


        Map<String, String> map = new HashMap<>();
//        map.put("/usr/add", "authc");
//        map.put("/usr/update", "authc");
        map.put("/usr/*""authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        //设置登录页面
        shiroFilterFactoryBean.setLoginUrl("/toLogin");

        return shiroFilterFactoryBean;
    }
...
}

测试,访问/usr/add/usr/update页面即可实现跳转到登录页面。

SpringBoot学习笔记(三)—— Shiro

4、Shiro 实现用户认证

4.1、用户认证

用户认证功能主要是通过Realm对象来实现的。我们先编写/login请求,封装用户登录数据,执行登录方法

@RequestMapping("/login")
public String login(String username, String password, Model model) {
    //获取当前的用户
    Subject subject = SecurityUtils.getSubject();
    //封装用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);

    try {
        subject.login(token); //执行登录方法,如果没有异常就说明登录成功
        return "index";
    } catch (UnknownAccountException e) { //用户名不存在
        model.addAttribute("msg""用户名错误");
        return "login";
    } catch (IncorrectCredentialsException e) { //密码错误
        model.addAttribute("msg""密码错误");
        return "login";
    }
}

login.html

绑定 action,显示信息

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>登录</h1>
<p th:text="${msg}" style="color: red"></p>
<hr>
<form th:action="@{/login}">
  <p>用户名:  <input type="text" name="username"></p>
  <p>密码:  <input type="password" name="password"></p>
  <p><input type="submit"></p>
</form>
</body>
</html>

重新启动,点击登录,在控制台可以看到执行了Realm中的认证。

SpringBoot学习笔记(三)—— Shiro

4.2、用户身份认证

用户实现登录功能,还需要进行校验,在UserRealm中完成认证功能。

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了========>认证");

    //用户名,密码 从数据中取
    String name = "root";
    String password = "123456";

    UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

    if (!userToken.getUsername().equals(name)) {
        //用户名不一致
        return null//抛出异常 UnknownAccountException
    }

    //密码验证,交给shiro实现
    return new SimpleAuthenticationInfo("", password, "");
}

启动测试,只有用户名和密码正确才能登录。

SpringBoot学习笔记(三)—— Shiro

5、Shiro 整合 Mybatis

5.1、导入相关包

包括druid数据源、jdbcmybatismysql

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.17</version>
</dependency>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

5.2、配置 mybatis 以及数据源

application.yml

spring:
    datasource:
        username: 用户名
        password: 密码
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&useUnicode=true&characterEncoding=utf-8
        type: com.alibaba.druid.pool.DruidDataSource

        #Spring Boot 默认是不注入这些属性值的,需要自己绑定
        #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:监控统计、log4j:日志记录、wall:防御sql注入
        #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

application.properties

mybatis.type-aliases-package=com.thomas.pojo
mybatis.mapper-locations=classpath:mapper/*.xml

5.3、编写实体类、mapper 相关接口和实现类

pojo/User.java

package com.thomas.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private int age;
    private String password;
}

mapper/UserMapper.java(接口)

package com.thomas.mapper;

import com.thomas.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface UserMapper {
    public User queryUserByName(String name);
}

Resources/mapper/UserMapper.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.thomas.mapper.UserMapper">
    <select id="queryUserByName" resultType="User">
        select * from mybatis.user where `name` = #{name}
    </select>
</mapper>

service/UserService.java(接口)

package com.thomas.service;

import com.thomas.pojo.User;

public interface UserService {
    public User queryUserByName(String name);
}

service/UserServiceImpl.java

package com.thomas.service;

import com.thomas.mapper.UserMapper;
import com.thomas.pojo.User;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserServiceImpl implements UserService {
    @Resource
    UserMapper userMapper;
    @Override
    public User queryUserByName(String name) {
        return userMapper.queryUserByName(name);
    }
}

最后我们来测试一下:

SpringBoot学习笔记(三)—— Shiro

5.4、在 Shiro 中使用 mybatis

从数据库中获取用户名和密码。

package com.thomas.config;

import com.thomas.mapper.UserMapper;
import com.thomas.pojo.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;

import javax.annotation.Resource;

public class UserRealm extends AuthorizingRealm {
    @Resource
    UserMapper userMapper;
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了===>授权");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了========>认证");

        UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

        //用户名,密码 从数据库中取
        User user = userMapper.queryUserByName(userToken.getUsername());
        if (user == null) {
            //用户名不一致
            return null//抛出异常 UnknownAccountException
        }

        //密码验证,交给shiro实现
        return new SimpleAuthenticationInfo("", user.getPassword(), "");
    }
}

SpringBoot学习笔记(三)—— Shiro

我们使用数据库中的用户名进行登录。

SpringBoot学习笔记(三)—— Shiro

6、Shiro 完成授权

6.1、拦截授权

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier(value = "webSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    //设置安全管理器
    shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

    //添加shiro的内置过滤器
    /*
        * anon: 无需认证就可以访问
        * authc: 必须认证了才能访问
        * user: 必须拥有 记住我 功能才能用
        * perms: 拥有对某个资源的权限才能访问
        * role: 拥有某个角色权限才能访问
        * */


    Map<String, String> map = new HashMap<>();
    //        map.put("/usr/add", "authc");
    //        map.put("/usr/update", "authc");
    //授权,正常的情况下,没有授权会跳转到未授权页面
    map.put("/usr/add""perms[user:add]");
    map.put("/usr/*""authc");

    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

    //设置登录页面
    shiroFilterFactoryBean.setLoginUrl("/toLogin");

    return shiroFilterFactoryBean;
}

这里我们进行授权,perms[user:add]表示必须是user用户有add权限才能授权访问。

但是我们登录之后访问localhost:8080/usr/add,仍然能正常访问。其实问题就在于,我们在ShiroFilterFactoryBean中先过滤再授权了,这样导致所有的请求都通过了。这时可能有小伙伴发现了,不对呀,我们明明是先写的map.put("/usr/add", "perms[user:add]");,然后才写的map.put("/usr/*", "authc");,这样应该是先授权了再过滤呀。

**这其实是因为 HashMap 不保证插入顺序,我们这里需要按照顺序插入 key,因此需要采用LinkedHashMap**,从下图也能看出两者的区别。

SpringBoot学习笔记(三)—— Shiro
image-20230504133713743

我们将上面的HashMap改为LinkedHashMap。重新启动,因为没有授权,所以无法访问 add 页面。

SpringBoot学习笔记(三)—— Shiro

6.2、设置未授权页面

当用户未授权时,应该跳转到未授权页面,所以我们需要设置相关页面。

controller层

@RequestMapping("/unauthorized")
@ResponseBody
public String toUnAuthorized() {
    return "未经授权无法访问此页面";
}

ShiroConfig.java

//设置未授权请求
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

SpringBoot学习笔记(三)—— Shiro

6.3、设置授权

我们先修改一下数据库的结构,加入一列perms,用来表示用户的授权情况,对于root用户,我们给予两个页面的权限

SpringBoot学习笔记(三)—— Shiro

修改一下对应的实体类,加入 perms 字段。

package com.thomas.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private int age;
    private String password;
    private String perms;
}

我们应该如何给当前用户设置权限呢?还记得之前说过的Shiro的三大对象吗,我们可以通过Subject来获得当前对象。在认证阶段,通过认证后我们返回User对象

...

public class UserRealm extends AuthorizingRealm {
    @Resource
    UserMapper userMapper;

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        ...

        //密码验证,交给shiro实现
        return new SimpleAuthenticationInfo(user, user.getPassword(), "");
    }
}

在授权时,我们通过 Subject 获取 User 对象,然后查询到数据库中对应用户的权限。这里我们需要考虑权限设置为 null 的情况以及多个权限的情况。

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了===>授权");
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    Subject subject = SecurityUtils.getSubject();
    User currentUser = (User) subject.getPrincipal(); //拿到User对象

    //使用当前数据库中查询出来的权限
    String perms = currentUser.getPerms();
    if (perms == null) {
        return null;
    }

    //可能存在多个权限
    //添加权限
    Arrays.stream(perms.split(",")).forEach(info::addStringPermission);

    return info;
}

将两个页面的请求都加入到拦截中。

Map<String, String> map = new LinkedHashMap<>();
map.put("/usr/add""perms[user:add]");
map.put("/usr/update""perms[user:update]");

重新启动,此时:

  • root 用户可以访问 add 和 update 页面
  • thomas1 用户仅可访问 add 页面
  • 其他用户无权限

总结

以上就是关于 Shiro 的学习内容了。从功能上看,Shiro 提供了与 SpringSecurity 高度相似的功能,相对而言,Shiro 会更简单,容易上手。

参考资料

[1]

【狂神说 Java】SpringBoot 最新教程 IDEA 版通俗易懂: https://www.bilibili.com/video/BV1PE411i7CV/?vd_source=f5b96c50144e9d1797a8c9056fefba8a

[2]

学习笔记: https://github.com/lzh66666/SpringBoot


原文始发于微信公众号(多肉罗罗):SpringBoot学习笔记(三)—— Shiro

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/186214.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!