SpringBoot
前言
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环境 |
@ConditionalOnJndi | JNDI存在指定项 |
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 | 请求示例 |
---|---|---|---|---|
@RequestParam | url | GET | 所有 | chgName?name=张三 |
@RequestBody | Body | POST/PUT/DELETE/PATCH | json | {“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. 源码追踪
-
controller
-
获取 subject 对象、AuthenticationToken 对象,并将认证所需的账号、密码存入 AuthenticationToken对象 (UsernamePasswordToken)
-
Subject.login(token)
进行登录,其会自动委托给 SecurityManager
SecurityManager : 负责真正的身份验证逻辑,会委托给 Authenticator 进行身份验证。
接下来,就来看 Subject.login(token) 之后会经历哪些过程
-
-
SecurityManager验证过程:
-
AbstractAuthenticator
authenticate
对传入的token开启身份验证之旅
-
进入到 ModularRealmAuthenticator 的
doAuthenticate
验证给定的token
这一步会开始调用数据库的数据,读取到账号、密码等信息,在获取到数据库信息之后,就会进入 MyRealm 开始比对
-
查询完数据库数据之后,会进入 MyRealm,这里对数据库数据进行基本的判断之后,会将用户信息封装为AuthenticationInfo对象
-
进入 AuthenticatingRealm,
getAuthenticationInfo
获取 MyRealm封装的用户信息,并assertCredentialsMatch
断言——请求的用户信息是否匹配数据库用户信息
-
接着回到 ModularRealmAuthenticator,执行
doSingleRealmAuthentication
通过与单个配置的realm交互来执行身份验证尝试
-
然后到 AbstractAuthenticator 的
authenticate
,获取到已经经过一些验证的登录人员信息,并进行一些判断
-
然后进入 AuthenticatingSecurityManager,这是后就会委托给给 Authenticator 进行认证
-
接着,会进入 DefaultSecurityManager 的
login
,先判断 Authenticator 是否认证成功,如果成功,就构造一个表示已验证帐户身份的实例
-
然后进入 DelegatingSubject 的
login
,将已经认证成功的用户相关信息存入session
-
-
controller
- 最后回到controller继续执行handler
- 最后回到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 被调用之前
- 在调用 Realm 的
getAuthenticationInfo
方法之前 - 在调用 Realm 的
getAuthenticationInfo
方法之后 - 在所有 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"; }
- cookie属性设置
测试:
- 先登录
- 然后重新访问,直接访问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可通过 Realm 的 doGetAuthorizationInfo
进行判断,触发判断的方式有:
- 前端页面的 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. 页面
相关文章
- SpringBoot引入freemaker前端模板
- SpringBoot实现电子文件签字+合同系统!
- SpringBoot 启动参数设置环境变量、JVM参数、tomcat远程调试
- 总在说SpringBoot内置了tomcat启动,那它的原理你说的清楚吗
- SpringBoot整合MyBatis(注解版)
- JAVA 离线安装配置Springboot
- springboot 注解
- 【SpringBoot2】02-SpringBoot中如何修改依赖的版本
- springboot下mabtis分页使用
- SpringBoot入门五(整合之端口和静态资源)
- springboot+vue闲一品交易平台源码(前后端分离项目)
- springboot通过Referer防止跨站点请求伪造
- 让我们大声说:HelloSpringBoot(“最易懂得SpringBoot学习”)
- 【SpringBoot】仿 spring-boot-project 自定义 starters