zl程序教程

您现在的位置是:首页 >  后端

当前栏目

SpringBoot

2023-09-11 14:22:32 时间

前言

springboot: springboot整合了所有框架,并默认配置了很多框架的使用方式。

本文主要是从实际开发的角度来写springboot相关的内容,当前并未更新完,会持续更新。

1. 基础

1. 基础依赖

<dependencies>
    <!--核心依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--导入配置文件处理器,配置文件进行绑定就会有提示-->
    <dependency>
        groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

<!-- 
	这个插件,可以将应用打包成一个可执行的jar包;
	直接使用 java -jar 进行打包
-->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
  • spring-boot-starter
    • springboot场景启动器,帮我们导入模块正常运行所依赖的组件
    • springboot会将所有的功能场景都抽取出来,做成一个个starter,只需要在项目中引入starter,相关的所有依赖都会自动导入

2. SpringBootApplication注解

@SpringBootApplication 
	- 标注一个主程序类,说明这是一个springboot应用
    - springboot运行改类的main方法来启动springboot应用
    - 由三个注解构成(除去四个元注解之外):@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan
  • @SpringBootConfiguration
    	- 标注在类上,表明这是一个springboot配置类
        - 底层是@Configuration
    
  • @EnableAutoConfiguration
    	- 开启自动配置功能的注解
        - 其底层由两个注解构成:
            - @AutoConfigurationPackage 自动配置包
              将添加该注解的类所在的package作为 自动配置package 进行管理
            - @Import({AutoConfigurationImportSelector.class}) 
              - 将 AutoConfigurationImportSelector类 导入到spring容器中
              - AutoConfigurationImportSelector类 可以帮助springboot应用将所有符合条件(@Conditional)的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器
              	- 需要导入的组件会以全类名的方式返回,然后添加到容器中;同时会给容器中加入很多自动配置类 xxxAutoConfiguration(免去我们手动配置的过程)
              	- springboot会在启动的时候从类路径 META-INF/spring.factories 中获取 EnableAutoConfiguration 指定的值,将这些值自动配置导入容器中
    
  • @ComponentScan
    	- 定义扫描的路径从中找出标识了需要装配的类自动装配到spring的bean容器中
    	- 默认装配标识了@Component注解的类到spring容器中
    	- @Component的派生注解有:@Controller@Service@Repository
    

3. Conditional注解

@Conditional
	- 按照一定的条件进行判断,满足条件给容器注册bean,配置配里面的内容才生效
	- 加在方法上(和@Bean搭配使用): 表明要满足对应条件,才注册此方法的组件
	- 加在类上(搭配@Component使用): 表明要满足对应条件,此配置类才会生效

派生注解:

注解说明
@ConditionalOnJava系统的java版本符合要求
@ConditionalOnBean容器中存在指定的Bean
@ConditionalOnMissingBean容器中不存在指定的Bean
@ConditionalOnExpression满足SpEL表达式指定
@ConditionalOnClass系统中有指定的类
@ConditionalOnMissingClass系统中没有指定的类
@ConditionalOnSingleCandidate容器中只有一个指定的Bean,或者这个Bean是首选Bean
@ConditionalOnProperty 系统中指定的属性有指定的值
@ConditionalOnResource 类路径下存在指定资源文件
@ConditionalOnWebApplication 当前是web环境
@ConditionalOnNotWebApplication当前不是web环境
@ConditionalOnJndiJNDI存在指定项

5. Bean注解

@Bean
	- 用于方法方法上:产生一个Bean对象,然后这个Bean对象交给Spring管理
		- 产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中
		- 搭配@Scope("prototype") 可以使得方法被多次调用,每次都实例一个Bean对象
	- 用于注解上:在运行时提供注册

4. yml配置文件

1.格式

格式:key: value

  • 缩进:

    # 对象:
    man:
     tom:
      name: 汤姆
      age: 17
    # 数组
    roles:
     - jack
     - rose
    
  • 字符串默认不加引号

    • ‘’:会转义特殊字符:a: '\n' 输出:\n
    • “”:不会转义特殊字符:a: '\n' 输出:换行
  • 占位符

    num: 16
    human:
     name: 李四
    
    man:
     name: ${human.name}
     age: ${num}
     salary: ${random.int} 
    

2.加载与读取

加载配置文件:

  • 加载指定的配置文件:@PropertySource(value = {"classpath: 配置文件名.后缀"})

  • 导入Spring的配置文件(springmvc.xml、spring.xml):@ImportResource( locations = "classpath = bean.xml" )放在主入口函数的类上

读取配置文件:

  • 类读取配置文件 @ConfigurationProperties(prefix = "xxx")

    tom:
     name: 汤姆
     num: 17.00
    
    /**
     * @Component 注册为一个组件,只有容器中的组件才能享有springboot提供的各种功能
     * @ConfigurationProperties 
     * 		- 将该类中的所有属性与配置文件中的相关配置进行绑定
     *		- prefix = "xxx" 与配置文件中哪一个key下的所有属性进行一一映射
     */
    @PropertySource(value = {"classpath: person.yml"})
    @Component
    @ConfigurationProperties(prefix = "tom")
    public class Tom {
        private String name;
        private double num;
    }
    
  • 属性读取配置文件 @Value("${xxx}")

    tom:
     name: 汤姆
     num: 17.00
    
    /**
     * @Value("${}") 获取对应属性文件中定义的属性值
     * @Value("#{}") 表示 SpEl 表达式通常用来获取 bean 的属性,或者调用 bean 的某个方法
     *
     */
    @Component
    public class Tom {
        @Value("${tom.name}")
        private String name;
        @Value(@Value("#{ T(java.lang.Math).random() * 100.0 }"))
        private String age;
    }
    

3.profile

在实际项目中,配置文件可能会有多个,如下图,这时候就需要通过spring.profile.active指定生效的文件了

spring:
  profiles:
    active: dev,datasource,websecurity,token

4.加载

配置文件的加载优先级(从高到低):
springboot启动后会扫描application.properties或者application.yml文件作为springboot的默认配置文件

  • 高优先级的配置文件内容会覆盖低优先级的

  • springboot中的classpath路径为

    • src/main/java
    • src/main/resouces
  • springboot中的classpath*除了上述两个路径,还包含:

    • 第三方jar包的根路径
  • file: ./config/
  • file:./
  • classpath:/config/
  • classpath:./

5. Slf4j日志

依赖:

        <!--提供Slf4j日志-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

使用:

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LogDemo {
	public static String test() {
		logger.trace("trace日志");
        logger.debug("debug日志");
        logger.info("info日志");
        logger.warn("warn日志");
        logger.error("error日志");
        return "日志系统";
	}
}
  • 默认日志级别是:info
  • 可在配置文件中通过如下配置设置默认日志级别
    logging:
      level:
        com:
          example:
            demo: warn
    

2.接口与过滤拦截

1. json接口

1. 代码实例

代码:

import org.springframework.web.bind.annotation.*;

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

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-25 15:27
 */
@RestController // 这里也可以直接用@Controller
@RequestMapping("/boot")
public class Controller {
    @PostMapping("/chgGend")
    public Object chgGend(@RequestBody Map<String, Object> params) {
        Object gend = params.get("gend");
        if (gend.equals("1")) {
            params.put("gend", "男");
        } else if (gend.equals("0")) {
            params.put("gend", "女");
        } else {
            params.put("gend", "性别未知");
        }
        return params;
    }

    @PostMapping("/chgName")
    public Map<String, Object> chgName(
            @RequestParam(value = "gend", required = false, defaultValue = "男") String gend,
            @RequestParam("name") String name) {
        Map<String, Object> res = new HashMap<>();
        String resName =  name == "张三" ? "法外狂徒" : name;
        res.put("name", resName);
        res.put("gend", gend);
        return res;
    }
}

接口测试:

2. RequestBody注解和RequestParam注解

@RequestBody
	- 主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的)
	- 一般都用POST方式进行提交
	- 一个请求,只有一个RequestBody
	- 可以和@RequestParam()同时使用

@RequestParam
	- 获取请求中的参数(不支持post请求)
	- value 请求中的参数名
	- required 是否必传该参数
	- defaultValue 当请求中没有传递该参数的时候的默认值(需要与required 搭配使用)

两者对比:

注解支持的类型支持的请求类型支持的Content-Type请求示例
@RequestParamurlGET所有chgName?name=张三
@RequestBodyBodyPOST/PUT/DELETE/PATCHjson{“name”: “张三”}

2.Filter

1.代码实例

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-25 16:18
 */
@Slf4j
@Order(1)
@WebFilter(urlPatterns = "/*")
public class RequestFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("过滤器初始化......");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("过滤器执行......");
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        log.info("过滤器销毁......");
    }
}

记得在主程序上加上注解@ServletComponentScan,不然过滤器无法生效。

启动项目,过滤器初始化:

发送请求,过滤器执行:

结束程序,过滤器销毁:

2. 小结

  • 过滤器工作流程:

  • 注解:

    @WebFilter(urlPatterns = "/*") 
    	- 将一个类声明为过滤器,该注解将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为过滤器
    	- 常用属性
    		- urlPatterns 指定一组过滤器的url匹配模式
    		- value等同于urlPatterns,但两者不能同时使用
    		- servletNames 指定过滤器将应用于哪些servlet
    		以上三个属性,至少得使用一个
    		- initParams 指定一组过滤器初始化参数
    
    @Order(1) 
    	- 设置过滤器的优先级,数值越小,优先级越高
    
  • 方法:

    • init

      • 初始化过滤器,可以在init()方法中获取Filter中的初始化参数
      • 只在应用启动的时候执行一次
    • doFilter

      • 完成过滤操作,当请求发过来的时候,过滤器将执行doFilter方法
      • 每次发送请求都会执行
    • destroy

      • Filter对象创建后会驻留在内存,当web应用移除或服务器停止时调用destroy()方法进行销毁
      • 仅执行一次,执行后可以释放过滤器占用的资源

3.在配置类中注册filter

public class RequestFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("过滤器初始化......");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("过滤器执行......");
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        log.info("过滤器销毁......");
    }
}
import com.example.sb_demo.common.filter.RequestFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-25 16:52
 */
@Configuration
public class WebConfiguration {
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new RequestFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.addInitParameter("paramName", "paramValue");
        registrationBean.setName("RequestFilter");
        registrationBean.setOrder(1);
        return registrationBean;
    }
}

可以在配置类中注册多个过滤器,通过setOrder指定其优先级,如下:

    @Bean
    public FilterRegistrationBean filterRegistrationBean1() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new RequestFilter());
        ...
        registrationBean.setOrder(1);
        return registrationBean;
    }
    @Bean
    public FilterRegistrationBean filterRegistrationBean1() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new AnotherFilter());
        ...
        registrationBean.setOrder(2);
        return registrationBean;
    }

3. Interceptor

拦截器类(implements HandlerInterceptor):

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-26 7:58
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("执行了拦截器的preHandle方法");
        String userName = request.getParameter("name");
        Object password = request.getParameter("password");
        if (userName.equals("张三") && password.equals("123456")) {
            log.info("身份验证通过");
            return true;
        }
        return false;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("执行了拦截器的postHandle方法");
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("执行了拦截器的afterCompletion方法");
    }
}

WebMvcConfig(implements WebMvcConfigurer

import com.example.sb_demo.common.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-26 8:07
 */
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration registration = registry.addInterceptor(new LoginInterceptor());
        /*
            addPathPatterns 需要拦截的url
            excludePathPatterns 不需要拦截的url
         */
        registration.addPathPatterns("/boot/**");
    }
}

测试:

1. 代码实例

2. 小结

过滤器与拦截器的执行流程:

拦截器的运行流程:

拦截器(Interceptor):类似于filter,都是面向切面编程。

  • preHandler 在 Interceptor 类中最先执行,用来进行一些前置初始化操作或是对当前请求做预处理,也可以进行一些判断来决定请求是否要继续进行下去

    • 返回 false,请求结束,后续的 Interceptor 和 Controller 都不会再执行
    • 返回 true,继续调用下一个 Interceptor 的 preHandle 方法,如果已经是最后一个 Interceptor 的时候就会调用当前请求的 Controller 方法
  • postHandler 在当前请求处理完成之后,也就是 Controller 方法调用之后执行,但是它会在 DispatcherServlet 进行视图返回渲染之前被调用,可以对 Controller 处理之后的 ModelAndView 对象进行操作

  • afterCompletion 在当前对应的 Interceptor 类的 postHandler 方法返回值为 true 时才会执行,在 DispatcherServlet 渲染了对应的视图之后执行,主要用来进行资源清理

4. 整合mybatis-plus

前言

  • 此部分只演示自定义SQL的查询(也就是用mybatis-plus来写mybatis,因为我这样我就不用再写springboot集成mybatis了,毕竟mybatis-plus只是对于mybatis的扩展)
  • 想深入了解springboot集成mybatis-plus的一些特性和操作,请移步我的这篇文章:MyBatis-Plus

1.依赖 + yml配置

依赖:

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!--mysql依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>
        <!--druid数据源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.11</version>
        </dependency>

配置:

spring:
  # 配置数据源信息
  datasource:
    # 配置数据源类型
    type: com.alibaba.druid.pool.DruidDataSource
    # 配置连接数据库的信息
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo1?serverTimezone=GMT&characterEncoding=utf-8&useSSL=false
    username: root
    password: admin

# 配置打印mybatis-plus的SQL执行日志
mybatis-plus:
  # 配置实体类所在包
  type-aliases-package: com.example.mp_demo.pojo
  # 配置mapper.xml路径
  mapper-locations: classpath:/mapper/*.xml
  configuration:
    # org.apache.ibatis.logging.stdout.StdOutImpl 可以打印sql、参数、查询结果
    # org.apache.ibatis.logging.log4j.Log4jImpl   不打印查询结果
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置端口
server:
  port: 8080

实体类:

import java.io.Serializable;
import lombok.Data;
/**
* 用户表
* @TableName user
*/
@Data
public class User implements Serializable {
    private Integer id;
    private String name;
    private Integer age;
    private String status;
    private String gender;
}

2.ResultBean

package com.example.mp_demo.common.entity;

import lombok.Data;

import java.io.Serializable;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-26 10:12
 */
@Data
public class ResultBean <T> implements Serializable {
    private static final long serialVersionUID = 1L;

    public static final int SUCCESS = 1;
    public static final int FAIL = -1;
    public static final int NO_LOGIN = -2;
    public static final int NO_PERMISSION = -3;

    private int code = SUCCESS;
    private String message = "success";
    private transient T data;

    public ResultBean(){
        super();
    }

    public ResultBean(T data){
        super();
        this.data = data;
    }

    public boolean isSuccess() {
        return SUCCESS == this.code;
    }
}

3.mapper.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.example.mp_demo.mapper.UserMapper">
    <select id="getAll" resultType="user">
        select
            id,
            name,
            age,
            status,
            gender
        from
            user
        where
            status = '1'
    </select>
</mapper>

4.UserMapper接口

package com.example.mp_demo.mapper;

import com.example.mp_demo.pojo.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import java.util.List;
import java.util.Map;

/**
* @author chen
* @description 针对表【user(用户表)】的数据库操作Mapper
* @createDate 2022-10-26 10:06:23
* @Entity com.example.mp_demo.pojo.User
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
    List<User> getAll();
}

5.UserService

package com.example.mp_demo.service;

import com.example.mp_demo.pojo.User;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;
import java.util.Map;

/**
* @author chen
* @description 针对表【user(用户表)】的数据库操作Service
* @createDate 2022-10-26 10:06:23
*/
public interface UserService extends IService<User> {
    List<User> getAll();
}
package com.example.mp_demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.mp_demo.pojo.User;
import com.example.mp_demo.service.UserService;
import com.example.mp_demo.mapper.UserMapper;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
* @author chen
* @description 针对表【user(用户表)】的数据库操作Service实现
* @createDate 2022-10-26 10:06:23
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{

    @Resource
    private UserMapper userMapper;

    @Override
    public List<User> getAll() {
        return userMapper.getAll();
    }
}

6.controller

package com.example.mp_demo.controller;

import com.example.mp_demo.common.entity.ResultBean;
import com.example.mp_demo.pojo.User;
import com.example.mp_demo.service.UserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

/**
 * @Author: chenJY
 * @Description:
 * @Date: 2022-10-26 10:09
 */
@RestController
@RequestMapping("user")
public class UserController {
    @Resource
    private UserService userService;

    @RequestMapping("getAll")
    public ResultBean getAll() {
        ResultBean resultBean = new ResultBean();
        List<User> data = userService.getAll();
        if (!data.isEmpty()) {
            resultBean.setCode(ResultBean.SUCCESS);
            resultBean.setData(data);
            resultBean.setMessage("成功获取数据");
        } else {
            resultBean.setCode(ResultBean.FAIL);
            resultBean.setMessage("获取数据失败");
        }
        return resultBean;
    }
}

测试: 不需要传参数。

3.整合shiro

1. shiro概述

Shiro: Java 的一个安全框架。

  • Authentication: 认证,验证当前用户是否本系统用户
  • Authorization: 授权,用户通过认证之后,验证是否有访问目标资源的权限
  • Session Management: 会话管理,用户登录之后,没退出之前,它的所有信息都在会话中
  • Cryptography: 加密,保证数据安全性,如密码加密
  • Web Support: 支持Web环境
  • Caching: 缓存
  • Concurrency: 支持多线程并发验证
  • Testing: 支持测试
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
  • Remember Me:一次登录后,下次再来的话不用登录了

从应用程序角度的来观察如何使用 Shiro 完成工作:

  • Subject: 代表当前 “用户”
  • SecurityManager: 安全管理器
  • Realm: Shiro 从 Realm 获取安全数据(如用户、角色、权限)

从 Shiro 内部来看下 Shiro 的架构:

  • Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现
  • Authorizer:授权器、即访问控制器
  • SessionManager:管理 Session 生命周期的组件
  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存
  • Cryptography:密码模块(加密解密)

2. 认证模块

1. 案例

1. 前期配置

依赖:

		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.4.1</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

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

配置文件:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spbt?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    username: root
    password: admin
shiro:
  loginUrl: /login

主程序类:

@SpringBootApplication
@MapperScan("com.chenjy.shirospbt.mapper")
public class ShirospbtApplication {

	public static void main(String[] args) {
		SpringApplication.run(ShirospbtApplication.class, args);
	}

}

数据表:

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '用户 ID',
  `name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户 名',
  `status` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '1' COMMENT '用户 状态 0禁用 1启用',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

mybatisx生成:

实体类:

@TableName(value ="user")
@Data
public class User implements Serializable {

    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private String status;
    private String password;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

mapper接口:

public interface UserMapper extends BaseMapper<User> {

}

2. shiro md5密码加密类

public class MD5Test {
    private static Map<String ,String> md5(String pwd) {
        Map<String ,String> map = new HashMap<>();
        map.put("md5加密", new Md5Hash(pwd).toHex());
        map.put("带盐md5加密", new Md5Hash(pwd, "chen").toHex());
        map.put("带盐md5迭代3次加密", new Md5Hash(pwd, "chen", 3).toHex());
        return map;
    }
    public static void main(String[] args) {
        md5("admin").forEach((key, value) -> {
            System.out.println(key + ":" + value);
        });
    }
}

将带盐加密三次的密码存入表:

3. service

根据传递的用户名从数据库查询用户数据。

public interface UserService extends IService<User> {
    User getUserInfoByName(String name);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
    @Resource
    private UserMapper userMapper;

    @Override
    public User getUserInfoByName(String name) {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("name", name);
        User user = userMapper.selectOne(wrapper);
        return user;
    }
}

4. Realm

@Data
@Slf4j
@Component
public class MyRealm extends AuthenticatingRealm {
    @Resource
    private UserService userService;

    /**
     * @Description
     *  - 自定义登录认证方法, shiro的login方法底层会调用该类的认证方法进行认证
     *  - 该方法只是获取进行对比的信息,认证逻辑还是shiro底层认证逻辑完成
     * @author chenJY
     * @param authenticationToken
     * @return AuthenticationInfo
    */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1 从token获取用户身份信息
        String name = authenticationToken.getPrincipal().toString();
        // 2 调用业务层获取用户信息(数据库中)
        User user = userService.getUserInfoByName(name);
        // 3 判断并将数据完成封装
        if (user == null) {
            log.error("用户不存在");
        } else if (!user.getStatus().equals("1")) {
            log.error("账号已失效");
        } else {
            /*封装用户信息为AuthenticationInfo对象*/
            AuthenticationInfo info = new SimpleAuthenticationInfo(
                    authenticationToken.getPrincipal(),
                    user.getPassword(),
                    ByteSource.Util.bytes("chen"),
                    authenticationToken.getPrincipal().toString()
            );
            log.info("用户信息无误,完成封装");
            return info;
        }
        return null;
    }
}

5. controller和ShiroConfig

controller:

@Controller
@Slf4j
public class ShiroController {
    /**
     * @Description 跳转到登录页面
     * @author chenJY
     * @return String
    */
    @RequestMapping("login")
    public String login() {
        return "login";
    }

    @RequestMapping("userLogin")
    public String userLogin(String name, String password,
                            @RequestParam(defaultValue = "false")boolean rememberMe,
                            HttpSession session){
        //1获取subject对象
        Subject subject  = SecurityUtils.getSubject();
        //2封装请求数据到token
        AuthenticationToken token = new UsernamePasswordToken(name,password,rememberMe);
        //3调用login方法进行登录认证
        try {
            subject.login(token);
            log.info("登陆成功");
            session.setAttribute("user",token.getPrincipal().toString());
            return "main";

        } catch (AuthenticationException e) {
            e.printStackTrace();
            log.info("登录失败");
            return "登录失败";
        }
    }
}

Shiro配置类:

@Configuration
public class ShiroConf {
    @Resource
    private MyRealm myRealm;

    /**
     * @Description 配置 SecurityManager
     * @author chenJY
     * @date 2022/11/3 9:27
     * @return DefaultWebSecurityManager
    */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        // 1 创建 defaultWebSecurityManager 对象
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 2 创建加密对象,并设置相关属性
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        // 2.1 采用 md5 加密
        matcher.setHashAlgorithmName("md5");
        // 2.2 迭代加密三次
        matcher.setHashIterations(3);
        // 3 将加密对象存储到 myRealm 中
        myRealm.setCredentialsMatcher(matcher);
        // 4 将 myRealm 存入 defaultWebSecurityManager 对象
        manager.setRealm(myRealm);
        // 5 返回
        return manager;
    }

    /**
     * @Description 配置 Shiro 内置过滤器拦截范围
     * @author chenJY
     * @return DefaultShiroFilterChainDefinition
    */
    @Bean
    public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();
        // 配置不用认证就可访问的公共资源
        Map<String ,String> map = new HashMap<>();
        map.put("/login", "anon");
        map.put("/userLogin", "anon");
        filterChainDefinition.addPathDefinitions(map);
        // 配置需要拦截的资源
        filterChainDefinition.addPathDefinition("/**", "authc");
        return filterChainDefinition;
    }
}

6. 页面

resources/templates

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<div class="main">
    <div class="title">
        <span>密码登录</span>
    </div>

    <div class="title-msg">
        <span>请输入登录账户和密码</span>
    </div>

    <form class="login-form" method="post" novalidate th:action="@{/userLogin}">
        <!--输入框-->
        <div class="input-content">
            <!--autoFocus-->
            <div>
                <input type="text"
                       autocomplete="off"
                       placeholder="用户名"
                       name="name"
                       required/>
            </div>

            <div style="margin-top: 16px">
                <input type="password"
                       autocomplete="off"
                       placeholder="登录密码"
                       name="password"
                       required
                       maxlength="32"/>
            </div>
        </div>

        <!--登入按钮-->
        <div style="text-align: center">
            <button type="submit" class="enter-btn" >登录</button>
        </div>

        <div class="foor">
             <div class="left">
                <div>记住用户:<input type="checkbox" name="rememberMe" value="true"></div>
            </div>
            <div class="right"><span>注册账户</span></div>
        </div>
    </form>

</div>
</body>
<style>
    body{
        background: #426258;
    }
    *{
        padding: 0;
        margin: 0;
    }

    .main {
        padding-left: 25px;
        padding-right: 25px;
        padding-top: 15px;
        width: 350px;
        height: 350px;
        background: #FFFFFF;
        /*以下css用于让登录表单垂直居中在界面,可删除*/
        position: absolute;
        top: 50%;
        left: 50%;
        margin: -175px auto 0 -175px;
    }

    .title {
        width: 100%;
        height: 40px;
        line-height: 40px;
    }

    .title span {
        font-size: 18px;
        color: #353f42;
    }

    .title-msg {
        width: 100%;
        height: 64px;
        line-height: 64px;
    }

    .title:hover{
        cursor: default	;
    }

    .title-msg:hover{
        cursor: default	;
    }

    .title-msg span {
        font-size: 12px;
        color: #707472;
    }

    .input-content {
        width: 100%;
        height: 120px;
    }

    .input-content input {
        width: 330px;
        height: 40px;
        border: 1px solid #dad9d6;
        background: #ffffff;
        padding-left: 10px;
        padding-right: 10px;
    }

    .enter-btn {
        width: 350px;
        height: 40px;
        color: #fff;
        background: #0bc5de;
        line-height: 40px;
        text-align: center;
        border: 0px;
    }

    .foor{
        width: 100%;
        height: auto;
        color: #9b9c98;
        font-size: 12px;
        margin-top: 20px;
    }

    .enter-btn:hover {
        cursor:pointer;
        background: #1db5c9;
    }

    .foor div:hover {
        cursor:pointer;
        color: #484847;
        font-weight: 600;
    }

    .left{
        float: left;
    }
    .right{
        float: right;
    }

</style>

</html>

main.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>主页</title>
</head>
<body>
<div class="con">
    <h1><span style="color: #67865d">用户:</span><span style="color: #be5d78" th:text="${session.user}"></span></h1>
    <br>
    <div class="component">
    </div>
    <br>
</div>
</body>
<style>
    body {
        background-color: #c1d3d0;
    }
    .con {
        text-align: center;
    }
    .component div {
        font-size: 32px;
    }
    .component span {
        color: #FFFFFF;
        font-size: 32px;
    }
</style>

7. 测试

3. shiro认证原理

1. 源码追踪

  1. controller

    • 获取 subject 对象、AuthenticationToken 对象,并将认证所需的账号、密码存入 AuthenticationToken对象 (UsernamePasswordToken

    • Subject.login(token) 进行登录,其会自动委托给 SecurityManager

      SecurityManager : 负责真正的身份验证逻辑,会委托给 Authenticator 进行身份验证。
      接下来,就来看 Subject.login(token) 之后会经历哪些过程

  2. SecurityManager验证过程:

    • AbstractAuthenticator authenticate 对传入的token开启身份验证之旅

    • 进入到 ModularRealmAuthenticatordoAuthenticate 验证给定的token

      这一步会开始调用数据库的数据,读取到账号、密码等信息,在获取到数据库信息之后,就会进入 MyRealm 开始比对

    • 查询完数据库数据之后,会进入 MyRealm,这里对数据库数据进行基本的判断之后,会将用户信息封装为AuthenticationInfo对象

    • 进入 AuthenticatingRealmgetAuthenticationInfo 获取 MyRealm封装的用户信息,并 assertCredentialsMatch 断言——请求的用户信息是否匹配数据库用户信息

    • 接着回到 ModularRealmAuthenticator,执行doSingleRealmAuthentication 通过与单个配置的realm交互来执行身份验证尝试

    • 然后到 AbstractAuthenticatorauthenticate,获取到已经经过一些验证的登录人员信息,并进行一些判断

    • 然后进入 AuthenticatingSecurityManager,这是后就会委托给给 Authenticator 进行认证

    • 接着,会进入 DefaultSecurityManagerlogin,先判断 Authenticator 是否认证成功,如果成功,就构造一个表示已验证帐户身份的实例

    • 然后进入 DelegatingSubjectlogin,将已经认证成功的用户相关信息存入session

  3. controller

    • 最后回到controller继续执行handler

2. 小结

认证流程:

  • subject.login(token) 进行登录,然后委托给 SecurityManager 进行身份验证
  • SecurityManager 又会委托给 Authenticator 进行身份验证
  • 但存在多Realm认证的时候,Authenticator 会委托给相应的 AuthenticationStrategy 进行多Realm验证(默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多realm验证)
  • Authenticator 将相应的 token 传入 Realm,从 Realm获取身份验证信息,若没有返回值,就表示身份验证失败(配置了多realm会按照相应的顺序及策略进行访问)

4. 多个Realm认证策略

1. 实现原理

当应用程序配置多个 Realm 时,Shiro 的 ModularRealmAuthenticator 会使用内部的 AuthenticationStrategy 组件判断认证是成功还是失败。

AuthenticationStrategy 组件:一个无状态的组件,它在身份验证尝试中被询问 4 次(这4 次交互所需的任何必要的状态将被作为方法参数):

  • 在所有 Realm 被调用之前
  • 在调用 RealmgetAuthenticationInfo 方法之前
  • 在调用 RealmgetAuthenticationInfo 方法之后
  • 在所有 Realm 被调用之后

将所有的 Realm 封装到一个 AuthenticationInfo 实例中,并返回,作为 subject 的身份信息。

Shiro中3种认证策略的实现(AuthenticationStrategy 接口的实现类):

实现类说明
AtLeastOneSuccessfulStrategy(默认)只要有一个(或更多)的 Realm 验证成功,那么认证将视为成功
FirstSuccessfulStrategy第一个 Realm 验证成功,整体认证将视为成功,且后续 Realm 将被忽略
AllSuccessfulStrategy所有 Realm 成功,认证才视为成功

2.代码实现

首先,自定义多个 realm (extends AuthenticatingRealm

然后,配置SecurityManeger

@Resource
private MyRealm myRealm;
@Resource
private MyRealm1 myRealm1;

@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
	// 1.创建DefaultWebSecurityManager对象
	DefaultWebSecurityManeger manager = new DefaultWebSecurityManager();
	// 2.创建认证对象 ModularRealmAuthenticator
	ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator;
	authenticator.setAuthenticationStrategy(new AllSuccessfulStrategy);
	manager.setAuthenticator(authenticator);
	// 3.封账Realm集合
	List<Realm> list = new ArrayList<>();
	list.add(myRealm);
	list.add(myRealm1);
	// 4.将realm存入DefaultWebSecurityManager
	manager.setRealms(list);
	// 5.返回
	return manager;
}

5. remember me

remember me: 让网站记住自己,下次访问就无须登录。

实现步骤:

  • 前端页面选中记住用户
  • 修改shiro配置类
    • cookie属性设置
          /**
           * @Description 设置cookie的基本属性
           * @author chenJY
           * @return SimpleCookie
          */
          public SimpleCookie rememberMeCookie() {
              SimpleCookie cookie = new SimpleCookie("rememberMe");
              // 设置跨域
              cookie.setPath("/");
              // 使得通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性
              cookie.setHttpOnly(true);
              // 设置cookie最大生存周期
              cookie.setMaxAge(60*60*24);
              return cookie;
          }
      
    • 创建shiro的cookie管理对象
          /**
           * @Description 创建shiro的cookie管理对象
           * @author chenJY
           * @return CookieRememberMeManager
          */
          public CookieRememberMeManager rememberMeManager() {
              CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
              rememberMeManager.setCookie(rememberMeCookie());
              // 设置对称密码秘钥,用于加密解密
              rememberMeManager.setCipherKey("123456".getBytes());
              return rememberMeManager;
          }
      
    • DefaultWebSecurityManager 设置rememberMe
              // 设置remember
              manager.setRememberMeManager(rememberMeManager());
      
    • Shiro 内置过滤器中添加存在用户过滤器(rememberMe)
      filterChainDefinition.addPathDefinition("/**", "user");
      
    • controller 新增一个跳转main.html的接口
          @RequestMapping("main")
          public String main() {
              return "main";
          }
      

测试:

  • 先登录
  • 然后重新访问,直接访问main

6. 退出登录

shiro过滤器可以很便捷地实现退出登录功能。

首先,在main.html中加入退出的按钮:

  • html

        <h2>
            <a href="/logout">退出</a>
        </h2>
    
  • css

        a {
            text-decoration: none;
        }
        /* 未访问的链接 */
        a:link {
            color: #7ac5a5;
        }
    
        /* 已访问的链接 */
        a:visited {
            color: #5fd29e;
        }
    
        /* 鼠标悬停链接 */
        a:hover {
            color: #56a6a6;
        }
    
        /* 已选择的链接 */
        a:active {
            color: #0fd7d7;
        }
    

然后,在shiro配置类的过滤器拦截方法shiroFilterChainDefinition中配置退出登录拦截器:

        // 配置退出拦截器
        filterChainDefinition.addPathDefinition("/logout", "logout");

演示:点击退出,直接退出。

3. 授权模块

1. 概述

用户经过认证之后,需要访问某些资源就需要经过授权过程。

shiro可通过 RealmdoGetAuthorizationInfo 进行判断,触发判断的方式有:

  • 前端页面的 shiro 属性
  • 接口中使用注解:@Requiresxxx
    • @RequiresAuthentication
      验证用户是否登录
      等同于方法 subject.isAuthenticated()
    • @RequiresUser
      验证用户是否被记忆
      等同于方法 subject.isRemembered()
    • @RequiresGuest
      验证是否是游客的请求
      即,subject.getPrincipal() 为 null
    • @RequiresRoles
      验证subject是否有相应角色,有角色访问方法,没有则会抛出异常
      subject中有相应的角色,才会去访问方法
    • @RequiresPermissions
      验证subject是否有相应权限,有权限访问方法,没有则会抛出异常

2. 案例

1. 表、实体类

表:

  • role 角色表

    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role`  (
      `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '角色 ID',
      `name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色 名',
      `status` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '1' COMMENT '角色 状态 0禁用 1启用',
      `nickname` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色别名',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
  • role_user 用户、角色映射表

    DROP TABLE IF EXISTS `role_user`;
    CREATE TABLE `role_user`  (
      `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '编号',
    	`rid` int(0) DEFAULT NULL COMMENT '角色 id',
    	`uid` int(0) DEFAULT NULL COMMENT '用户 id',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    

用户表、角色表、映射表:

2. mapper、service

mapper:

    @Select("select name from role where id in " +
            "(select rid from role_user where uid = " +
            "(select id from user where name = #{principal}))")
    List<String> getUserRoleInfo(@Param("principal") String principal);

service:

    @Override
    public List<String> getUserRoleInfo(String principal) {
        return userMapper.getUserRoleInfo(principal);
    }

3. 页面