朝小闇的博客

海上月是天上月,眼前人是心上人

SpringBoot(五)——SpringSecurity和Shiro

1.初始化导入资源

  • 新建项目并配置SpringWeb、Thymeleaf和SpringSecurity依赖(注:只要配置了SpringSecurity依赖后续资源在访问时就会自动被拦截并跳转到登录页,即使并没有配置登录账号);

  • 导入素材,资源链接:https://pan.baidu.com/s/1CsbZrhFKggnucYPHFVaHSw 提取码:v7g7

  • 新建Controller目录,下建RouterController.java路由控制器,实现简单路由即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Controller
    public class RouterController {
    @RequestMapping({"/","/index"})
    public String index(){
    return "index";
    }
    @RequestMapping("/toLogin")
    public String toLogin(){
    return "views/login";
    }
    @RequestMapping("/level1/{id}")
    public String level1(@PathVariable("id")Integer id){
    return "views/level1/"+id;
    }
    @RequestMapping("/level2/{id}")
    public String level2(@PathVariable("id")Integer id){
    return "views/level2/"+id;
    }
    @RequestMapping("/level3/{id}")
    public String level3(@PathVariable("id")Integer id){
    return "views/level3/"+id;
    }
    }

2.SpringSecurity权限及认证

SpringSecurity官方文档这里有简单的Security配置,按照此模式配置自己的config文件:

官方文档链接:https://docs.spring.io/spring-security/site/docs/5.3.8.RELEASE/reference/html5/#servlet-authentication-jdbc

image-20210311171442784

2.1 权限及认证

  • 新建SecurityConfig.java代码,实现权限及认证功能:

  • 这里主要是查看源码WebSecurityConfigurerAdapter.java文件中相关重写方法,照本临摹实现一些小功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
/* 查看源码中该方法进行类比重写
authorizeRequests()是授权请求
antMatchers()为不同路径设置访问权限
*/
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/*").hasRole("vip1")
.antMatchers("/level2/*").hasRole("vip2")
.antMatchers("/level3/*").hasRole("vip3");
// 没有授权则会默认进入登录页面,查看源码可知自动跳转/login
http.formLogin();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/* inMemoryAuthentication()是从内存中获取认证信息,还有jdbcAuthentication()从数据库中获取认证信息这里暂未连接数据库
从源码示例中拿出.withUser("user").password("password").roles("USER").and()模板使用
但是这种未加密密码传输安全受到威胁,需要使用passwordEncoder()加密,至于加密方式BCryptPasswordEncoder()则看需求
* */
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3").and()
.withUser("zhaoan").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2").and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}

有关于JDBC数据库连接认证看这里:可能本人后续也会具体讲解

image-20210311193523685

image-20210311193130575

2.2 定制登录页及注销

  • 定制登录页以及注销,修改上面的代码即可:
1
2
3
4
5
6
7
// 没有授权则会默认进入登录页面,查看源码可知自动跳转/login
// 定制跳转登录页,并绑定表单中用户名和密码
http.formLogin().loginPage("/toLogin").usernameParameter("username").passwordParameter("password");
// 开启注销及跳转功能
http.logout().logoutSuccessUrl("/");
// 可能需要开启防跨站get访问,注销时使用
http.csrf().disable();

并且修改自定义login.html页面表单提交:

1
2
<!-- Post请求是Security默认设置的认证,以及之后跳转,而不是RouterController实现 -->
<form th:action="@{/toLogin}" method="post">

2.3 Thymeleaf-SpringSecurity搭配使用注销及用户显示

添加依赖:

1
2
3
4
5
6
<!-- thymeleaf-SpringSecurity依赖,注意此版本可能过旧,只能使用SpringBoot2.0.9最高,但本人2.4.3可以正常使用 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>

index.html:初次之外记得上一点中设置的logout相关

且此处使用的ui为:semantic-ui,官网国内引入版:https://zijieke.com/semantic-ui/elements/icon.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--引入sec命名-->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<!--登录注销-->
<div class="right menu">
<!--未登录-->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="sign-in icon"></i> 登录
</a>
</div>

<!--已登录,显示用户名和注销-->
<div sec:authorize="isAuthenticated()">
<a class="item">
用户名:<span sec:authentication="name"></span></a>
</div>
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
</div>
</div>

2.4 权限分级显示

在每一栏上增加此权限分级认证语句即可

1
<div class="column" sec:authorize="hasRole('vip3')">

2.5 记住我功能

login.html:在提交按钮上面增加此复选框

1
2
3
<div class="field">
<input type="checkbox" name="remember">记住我
</div>

SecurityConfig.java

1
2
// 开启记住我且绑定表单参数
http.rememberMe().rememberMeParameter("remember");

3.Shiro整合使用

3.1 Shiro架构及初始化

  • Subject:
    • 应用代码直接交互的对象是Subject, 也就是说Shiro的对外API核心就是Subject,Subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给SecurityManager;;Subject其实是一个门面,SecurityManageer才是实际的执行者;
  • ShiroSecurityManager:
    • 安全管理器,即所有与安全有关的操作都会与SercurityManager交互,并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色;
  • Realm:
    • Shiro从Realm获取安全数据(如用户,角色,权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看成DataSource;

image-20210312105829143

  1. 新建项目,添加Web和Thymeleaf依赖,以及添加Shiro-Spring依赖:

    1
    2
    3
    4
    5
    6
    <!-- 引入shiro-spring依赖 -->
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.7.1</version>
    </dependency>
  2. 新建前端资源:

    • templates目录下新建index.html,user/vip1.html、user/vip2.html三个简单文件,只需添加文字表示各页面即可;
  3. 新建controller包,下建MyController.java文件,简单配置路由:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Controller
    public class MyController {
    @RequestMapping({"/","/index"})
    public String index(Model model){
    model.addAttribute("msg","hello,shiro");
    return "index";
    }
    @RequestMapping("/user/vip1")
    public String vip1(Model model){
    model.addAttribute("msg","hello,vip1");
    return "user/vip1";
    }
    @RequestMapping("/user/vip2")
    public String vip2(Model model){
    model.addAttribute("msg","hello,vip2");
    return "user/vip2";
    }
    }

    并对前端html文件设置Thymeleaf模板接收参数显示在页面上,并在首页增加跳转链接到其余两个页面,这个内容和SpringSecurity初始化资源差不多。

  4. 新建config包,下建UserRealm.java文件和ShiroConfig.java文件:

    UserRealm.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 自定义 UserRealm extends AuthorizingRealm
    public class UserRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    return null;
    }
    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    return null;
    }
    }

    ShiroConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Configuration
    public class ShiroConfig {
    // 1.创建realm对象,需要自定义类
    @Bean
    public UserRealm userRealm(){
    return new UserRealm();
    }
    // 2.DefaultWebSecurityManager @Qualifier("userRealm")表示绑定方法userRealm,有两种方式如下
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 关联userRealm
    securityManager.setRealm(userRealm);
    return securityManager;
    }
    // 3.ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    // 设置安全管理器
    bean.setSecurityManager(securityManager);
    return bean;
    }
    }

    此时就完成了简单的Shiro配置以及前端页面的链接跳转,向SpringSecurity一样,接下来其实就是登录、权限、注销等功能实现。

3.2 拦截器及跳转登录

  1. 增加登录页,新建login.html,实现简单登录表单:

    1
    2
    3
    4
    5
    6
    7
    <h1>登录</h1>
    <hr>
    <form action="">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="text" name="password"></p>
    <p><input type="submit"></p>
    </form>
  2. 设置路由,在MyController.java文件中增加,实现路由:

    1
    2
    3
    4
    @GetMapping("/login")
    public String login(){
    return "login";
    }
  3. ShiroConfig.java文件中shiroFilterFactoryBean()方法中增加过滤设置以及无权限跳转登录设置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 添加shiro内置过滤器
    /* anon 无需认证就可以访问
    authc 必须拥有认证才可以访问
    user 必须拥有记住我功能才能访问
    perms 拥有对某个资源的权限才能访问
    role 拥有某个角色权限才能访问
    */
    Map<String,String> filterMap = new LinkedHashMap<>();
    filterMap.put("/user/*","authc");

    bean.setFilterChainDefinitionMap(filterMap);
    // 设置登录请求,即拦截之后跳转页面
    bean.setLoginUrl("/login");

3.3 整合Mybatis、druid认证登录

  1. 添加依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <!-- lombok可自动注入有参无参构造等 -->
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    </dependency>
    <!-- 引入mysql -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- 引入mybatis -->
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
    </dependency>
    <!-- 引入druid -->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.4</version>
    </dependency>
    <!-- 引入log4j -->
    <dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.12</version>
    </dependency>

    如四中新建log4j.properties文件并初始化:

    1
    2
    3
    4
    log4j.rootLogger=DEBUG, stdout
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
  2. application.yml文件(没有该文件在application.properties同级创建该文件)中增加数据库连接druid数据源等配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    spring:
    datasource:
    username: root
    password: password
    # serverTimezone=UTC是时区,
    url: jdbc:mysql://localhost:3306/springboot_mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    # Spring默认不注入这些属性配置,需要自己绑定,一般根据公司需要个性绑定,也是druid专有属性
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    # 配置filter,stat:监控统计;log4j:日志记录;wall:防御sql注入
    filters: stat,wall,log4j
    maxPoolPreparedStatmentPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    # 整合mybatis,分别为实体类位置和mapper实现层位置,这里暂未使用,等会用得上
    mybatis:
    type-aliases-package: com.kun.pojo
    mapper-locations: classpath:mapper/*.xml
  3. MyController.java接收post请求:

    login.html

    1
    2
    3
    4
    5
    6
    7
    8
    <h1>登录</h1>
    <hr>
    <p th:text="${msg}" style="color: red"></p>
    <form action="/login" method="post">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="password" name="password"></p>
    <p><input type="submit"></p>
    </form>

    MyController.java增加post请求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 接收表单数据并封装成token存入subject中,传给UserRealm中认证
    @PostMapping("/login")
    public String loginAuthentication(
    @PathVariable("username") String username,
    @PathVariable("password") String password,
    Model model){
    // 获取当前用户
    Subject subject = SecurityUtils.getSubject();
    // 封装当前用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username,password);
    try {
    // 将该用户token传给认证doGetAuthenticationInfo
    subject.login(token);
    }catch (UnknownAccountException e){
    model.addAttribute("msg","用户名错误");
    return "login";
    }catch (IncorrectCredentialsException e){
    model.addAttribute("msg","密码错误");
    return "login";
    }
    return "index";
    }
  4. 新建pojo、mapper和resources下mapper三个包:

    pojo.User.java

    1
    2
    3
    4
    5
    6
    7
    8
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    private int id;
    private String name;
    private String pwd;
    }

    mapper.UserMapper.java

    1
    2
    3
    4
    5
    @Mapper
    @Repository
    public interface UserMapper {
    User getUserByName(String name);
    }

    mapper.UserMapper.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?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">
    <!-- 注意此处需要设置连接位置 这里使用连接的数据库中的user表,表中属性为id,user,pwd-->
    <mapper namespace="com.kun.mapper.UserMapper">
    <select id="getUserByName" parameterType="String" resultType="User">
    select * from user where name = #{name}
    </select>
    </mapper>
  5. 再建service包(服务层,具体各层以及常用注解请参看本人博客SpringBoot原理),下建UserService.javaUserServiceImpl.java两个文件:

    UserService.java

    1
    2
    3
    4
    public interface UserService {
    User getUserByName(String name);
    }

    UserServiceImpl.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Service
    public class UserServiceImpl implements UserService{
    @Autowired
    UserMapper userMapper;

    @Override
    public User getUserByName(String name) {
    return userMapper.getUserByName(name);
    }
    }
  6. UserRealm.java实现认证即可正常使用数据库用户登录:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 自定义 UserRealm extends AuthorizingRealm
    public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    return null;
    }
    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行了=>认证doGetAuthenticationInfo");
    // 将传入的认证token转换成Controller中封装的原token
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;

    // 连接真实数据库获取数据
    User user = userService.getUserByName(userToken.getUsername());
    // 没有此人只要返回null即可,会自动将其识别为UnknownAccountException回到
    if(user==null){
    return null;
    }
    // 密码认证,shiro自己做,而不需要自己接触密码,SimpleAuthenticationInfo使用简单认证,没有加密,可以尝试加密
    // 查看SimpleAuthenticationInfo源码实现加密传输密码,md5,md5盐值加密
    return new SimpleAuthenticationInfo("",user.getPwd(),"");
    }
    }

3.4 授权

  1. 修改user表,增加perms权限一栏,并为部分用户授权:

    image-20210312151131121

  2. 增加pojo下User实体类属性perms:

    1
    private String perms;
  3. MyController.java文件中增加无权限访问路由:

    1
    2
    3
    4
    5
    6
    // @ResponseBody 是返回字符串到空白页面
    @RequestMapping("/noauth")
    @ResponseBody
    public String noauth(){
    return "没有权限无法访问";
    }
  4. ShiroConfig.java文件中设置权限拦截以及无权限时跳转页面:

    1
    2
    3
    4
    5
    6
    /*filterMap.put("/user/*","authc");*/
    // 设置权限拦截
    filterMap.put("/user/vip1","perms[user:vip1]");
    filterMap.put("/user/vip2","perms[user:vip2]");
    // 设置无权限跳转页面
    bean.setUnauthorizedUrl("/noauth");
  5. UserRealm.java文件认证doGetAuthenticationInfo()方法中修改返回值方法传参:

    1
    2
    // 第一个参数为principal,将该参数存到subject中,由授权获取
    return new SimpleAuthenticationInfo(user,user.getPwd(),"");

    对授权doGetAuthorizationInfo()方法作如下操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    System.out.println("执行了=>授权doGetAuthorizationInfo");
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    Subject subject = SecurityUtils.getSubject();

    // 获取当前用户Subject的Principals,即为认证最后返回存储的user
    User currentUser = (User)subject.getPrincipal();
    // 为当前info增加当前Subject的权限,set可以设置一个集合的权限
    info.addStringPermission(currentUser.getPerms());

    return info;
    }

    即可成功授权访问。

3.5 shiro-thymeleaf

  1. 在maven仓库查询thymeleaf-shiro,添加最新版本进入依赖:

    pom.xml

    1
    2
    3
    4
    5
    6
    <!-- thymeleaf-shiro -->
    <dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
    </dependency>
  2. ShiroConfig.java文件中增加方法整合二者,并加入bean,没有此方法html中引用shiro不会成功:

    1
    2
    3
    4
    5
    // 整合shiro-thymeleaf
    @Bean
    public ShiroDialect getShiroDialect(){
    return new ShiroDialect();
    }
  3. 修改首页index.html:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org"
    xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
    <head>
    <meta charset="UTF-8">
    <title>首页</title>
    </head>
    <body>
    <h1>首页</h1>
    <div th:text="${msg}"></div>
    <!-- 从session中判断值 -->
    <div th:if="${session.userLogin==null}">
    <a th:href="@{/login}">登录</a>
    </div>
    <hr>
    <!-- 使用thymeleaf-shiro获取用户权限 -->
    <div shiro:hasPermission="user:vip1">
    <a th:href="@{/user/vip1}">vip1</a>
    </div>
    <div shiro:hasPermission="user:vip2">
    <a th:href="@{/user/vip2}">vip2</a>
    </div>
    </body>
    </html>
  4. MyController.java文件中loginAuthentication()方法中增加Session:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 接收表单数据并封装成token存入subject中,传给UserRealm中认证
    @PostMapping("/login")
    public String loginAuthentication(
    String username, String password,
    Model model){
    // 获取当前用户
    Subject subject = SecurityUtils.getSubject();
    // 封装当前用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username,password);
    try {
    // 将该用户token传给认证doGetAuthenticationInfo
    subject.login(token);
    }catch (UnknownAccountException e){
    model.addAttribute("msg","用户名错误");
    return "login";
    }catch (IncorrectCredentialsException e){
    model.addAttribute("msg","密码错误");
    return "login";
    }
    // 对当前subject用户设置session
    Session session = subject.getSession();
    session.setAttribute("userLogin",subject);
    return "index";
    }
-------- 本文结束 感谢阅读 --------