Spring Security
Authentication
基于内存
@Configuration
public class securityInMemoryUser {
@Bean
public UserDetailsService userDetailsService(){
UserDetails user1 = User.builder()
//创建用户名为eric的用户
.username("eric")
//创建密码123456并对密码进行加密
.password(passwordEncoder().encode("123456"))
//***重要:为eric用户设置角色为student,然后eric就拥有了ROLE_student的权限
.roles("student")
//为eric设置权限,此配置会覆盖上面一行的代码,也就是说eric的权限为student:delete和student:add,而没有了ROLE_student的权限
.authorities("student:delete","student:add")
.build();
UserDetails user2 = User.builder()
.username("tomas")
.password(passwordEncoder().encode("123456"))
.authorities("teacher:delete","teacher:add")
//这里会对上面进行覆盖
.roles("teacher")
.build();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
//设置SpringSecutiry密码加密的类,并将其注册为bean交给spring管理
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
基于数据库
思路:
- 配置数据库的相关信息,通过xml文件的方式写sql语句
- 加载用户信息,需实现UserDetailsService接口,并实现loadUserByUsername方法
- 由于loadUserByUsername方法需返回一个UserDetails类型的返回值,需要定义一个安全用户类SecurityUser来实现UserDetails接口,该安全用户类可以获取到用户名和密码以及权限等信息
- 编写安全用户SecurityUser类,实现所有方法,并将entity中的用户信息存入到安全用户类中
- 编写SecurityUserDetailsServiceImpl类,实现UserDetailsService,回到第二步,加载用户信息
- 接收到用户登录的用户名,通过用户名到数据库中查询是否存在用户,不存在抛异常
- 获取用户权限信息,通过上一步查询到的用户用户信息(包括id),到权限表中根据id,通过连接查询用户权限,返回List
的权限集合 - 由于安全用户中的权限类型必须是继承了GrantedAuthority接口的子类,所以我们需要通过将String转换为SimpleGrantedAuthority类型(实现了上述接口的子类之一)的数据
- 创建安全用户new,将第一步的用户信息封装到安全用户中,并设置权限信息为第3步转换后的权限
- 返回安全用户既可
创建表
msg:在数据库中创建用户表,角色表,权限表,以及对应的连接表,即用户与角色的连接表,角色与权限的连接表。共5张表。
示例数据:
用户1
:obama,角色
:管理员,权限
:学生查询,学生增加,学生修改,学生删除,教师查询,教师修改
用户2
:tomas,角色
:教师,权限
:学生查询,学生增加,学生修改,学生删除,教师添加
用户3
:eric,角色
:学生,权限
:学生查询,导出学生信息
用户表
msg:创建
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
-- 用户id
`user_id` int NOT NULL,
-- 用户名
`username` varchar(255) DEFAULT NULL,
-- 用户密码
`password` varchar(255) DEFAULT NULL,
-- 用户性别
`sex` varchar(255) DEFAULT NULL,
-- 用户是否可用
`enabled` int DEFAULT NULL,
-- 用户是否过期,1为正常,0为过期
`account_no_expired` int DEFAULT NULL,
-- 凭据/密码是否过期,1正常,0过期
`credentials_no_expired` int DEFAULT NULL,
-- 用户是否被锁定
`account_no_locked` int DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
msg: 示例数据(密码:123456)
INSERT INTO `sys_user` VALUES ('1', 'obama', '$2a$10$BsjlTIIXhrfig8q7t6evwuSvdl1yUe0dXU0g4HD0Jaj9Lu8s7yGmu', '男', '1', '1', '1', '1');
INSERT INTO `sys_user` VALUES ('2', 'tomas', '$2a$10$BsjlTIIXhrfig8q7t6evwuSvdl1yUe0dXU0g4HD0Jaj9Lu8s7yGmu', '男', '1', '1', '1', '1');
INSERT INTO `sys_user` VALUES ('3', 'eric', '$2a$10$BsjlTIIXhrfig8q7t6evwuSvdl1yUe0dXU0g4HD0Jaj9Lu8s7yGmu', '男', '1', '1', '1', '1');
角色表
msg:创建
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
-- 角色id
`id` int NOT NULL,
-- 角色名称
`rolename` varchar(255) DEFAULT NULL,
-- 角色含义
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
msg:示例数据
INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN', '管理员');
INSERT INTO `sys_role` VALUES ('2', 'ROLE_TEACHER', '老师');
INSERT INTO `sys_role` VALUES ('3', 'ROLE_STUDENT', '学生');
权限表
msg:创建
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
-- 权限id
`id` int NOT NULL AUTO_INCREMENT,
-- 该权限的父id
`pid` int DEFAULT NULL,
-- 权限名称
`name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '名称',
-- 权限编码,java中匹配的权限代码
`code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '权限编码',
-- 类型,0代表菜单,1代表权限,2
`type` int DEFAULT NULL COMMENT '0代表菜单1权限',
-- 删除标志
`delete_flag` tinyint DEFAULT '0' COMMENT '0代表未删除,1代表已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
msg:示例数据
INSERT INTO `sys_menu` VALUES ('1', '0', '学生管理', '/student/**', '0', '0');
INSERT INTO `sys_menu` VALUES ('2', '1', '学生查询', 'student:query', '1', '0');
INSERT INTO `sys_menu` VALUES ('3', '1', '学生添加', 'student:add', '1', '0');
INSERT INTO `sys_menu` VALUES ('4', '1', '学生修改', 'student:update', '1', '0');
INSERT INTO `sys_menu` VALUES ('5', '1', '学生删除', 'student:delete', '1', '0');
INSERT INTO `sys_menu` VALUES ('6', '1', '导出学生信息', 'student:export', '1', '0');
INSERT INTO `sys_menu` VALUES ('7', '0', '教师管理', '/teacher/**', '0', '0');
INSERT INTO `sys_menu` VALUES ('8', '7', '教师查询', 'teacher:query', '1', '0');
INSERT INTO `sys_menu` VALUES ('9', '7', '教师添加', 'teacher:add', '1', '0');
INSERT INTO `sys_menu` VALUES ('10', '7', '教师修改', 'teacher:update', '1', '0');
INSERT INTO `sys_menu` VALUES ('11', '7', '教师删除', 'teacher:delete', '1', '0');
INSERT INTO `sys_menu` VALUES ('12', '7', '导出教师信息', 'teacher:export', '1', '0');
用户角色连接表
msg:创建
DROP TABLE IF EXISTS `sys_role_user`;
CREATE TABLE `sys_role_user` (
-- 用户id
`uid` int DEFAULT NULL,
-- 角色id
`rid` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
msg:示例数据
INSERT INTO `sys_role_user` VALUES ('1', '1');
INSERT INTO `sys_role_user` VALUES ('2', '2');
INSERT INTO `sys_role_user` VALUES ('3', '3');
角色权限连接表
msg:创建
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
-- 角色id
`rid` int DEFAULT NULL,
-- 权限id
`mid` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
msg:示例数据
INSERT INTO `sys_role_menu` VALUES ('1', '2');
INSERT INTO `sys_role_menu` VALUES ('2', '2');
INSERT INTO `sys_role_menu` VALUES ('3', '2');
INSERT INTO `sys_role_menu` VALUES ('1', '3');
INSERT INTO `sys_role_menu` VALUES ('2', '3');
INSERT INTO `sys_role_menu` VALUES ('1', '4');
INSERT INTO `sys_role_menu` VALUES ('2', '4');
INSERT INTO `sys_role_menu` VALUES ('1', '5');
INSERT INTO `sys_role_menu` VALUES ('2', '5');
INSERT INTO `sys_role_menu` VALUES ('3', '6');
INSERT INTO `sys_role_menu` VALUES ('1', '8');
INSERT INTO `sys_role_menu` VALUES ('2', '9');
INSERT INTO `sys_role_menu` VALUES ('1', '10');
配置yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/yan
username: root
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.yannqing.entity
configuration:
#下划线命名转驼峰命名
map-underscore-to-camel-case: true
#输出记在日志文件中
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
实体类
User
用户实体类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.sql.Timestamp;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private Integer userId;
private String username;
private String password;
private String sex;
private String address;
private String phone;
private String email;
private int age;
private String avatar;
private String nickName;
private int enabled;
private int accountNoExpired;
private int credentialsNoExpired;
private int accountNoLock;
private Timestamp createTime;
private String description;
}
Permissions
权限实体类
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Permissions {
private Integer id;
private Integer pid;
private Integer type;
private String name;
private String code;
}
dao与xml
UserDao
import com.yannqing.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserDao {
/**
* 根据用户名获取用户信息
* @param userName
* @return
*/
User getByUserName(@Param("userName") String userName);
}
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.yannqing.dao.UserDao">
<select id="getByUserName" resultType="User">
# select user_id,username,password,sex,address,enabled,account_no_expired,credentials_no,expired,account
select * from sys_user where username=#{userName}
</select>
</mapper>
PermissionsDao
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PermissionsDao {
/**
* 根据用户id查询相关权限
* @param userId
* @return
*/
List<String> queryPermissionsByUserId(@Param("userId") Integer userId);
}
MenuMapper.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.yannqing.dao.PermissionsDao">
<select id="queryPermissionsByUserId" resultType="String">
select distinct sm.code from sys_role_user sru inner join sys_role_menu srm on sru.rid = srm.rid
inner join sys_menu sm on srm.mid = sm.id where sru.uid = #{userId}
</select>
</mapper>
内连接查询!!!涉及到多表查询!
Service
MenuService与UserService实现dao中对应的方法即可
SecurityUserDetailsServiceImpl
import com.yannqing.dao.PermissionsDao;
import com.yannqing.entity.User;
import com.yannqing.service.UserService;
import com.yannqing.vo.SecurityUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionsDao permissionsDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getByUserName(username);
if (user==null){
throw new UsernameNotFoundException("该用户不存在");
}
System.out.println(user);
//获取用户权限
List<String> permissions = permissionsDao.queryPermissionsByUserId(user.getUserId());
System.out.println(permissions);
//将String转为SimpleGrantedAuthority
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
authorities.add(simpleGrantedAuthority);
}
//获取SimpleGrantedAuthority集合List的第二种方法,通过流的形式获取
// List<SimpleGrantedAuthority> au = permissions.stream().map(SimpleGrantedAuthority::new).toList();
SecurityUser securityUser = new SecurityUser(user);
securityUser.setSimpleGrantedAuthorities(authorities);
return securityUser;
}
}
vo层
SecurityUser
import com.yannqing.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class SecurityUser implements UserDetails {
private final User user;
private List<SimpleGrantedAuthority> simpleGrantedAuthorities;
public void setSimpleGrantedAuthorities(List<SimpleGrantedAuthority> simpleGrantedAuthorities) {
this.simpleGrantedAuthorities = simpleGrantedAuthorities;
}
public SecurityUser(User user) {
this.user = user;
}
public User getUser() {
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//权限配置
return simpleGrantedAuthorities;
}
@Override
public String getPassword() {
String password = user.getPassword();
//擦除用户的密码,防止传送到前端。
user.setPassword(null);
return password;
}
@Override
public String getUsername() {
return user.getUsername();
}
//账户是否未过期,数据库中,0过期,1为正常
@Override
public boolean isAccountNonExpired() {
return user.getAccountNoExpired()==1;
}
//账户是否未锁定
@Override
public boolean isAccountNonLocked() {
return user.getAccountNoLock()==0;
}
//凭据/密码是否未过期
@Override
public boolean isCredentialsNonExpired() {
return user.getCredentialsNoExpired()==1;
}
//用户是否可用
@Override
public boolean isEnabled() {
return user.getEnabled()==1;
}
}
config层
SecurityConfig
import com.yannqing.security.filter.JwtAuthenticationTokenFilter;
import com.yannqing.security.LoginFailureHandler;
import com.yannqing.security.LoginSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize)->authorize
.requestMatchers(HttpMethod.POST,"/login").permitAll()
.requestMatchers(HttpMethod.POST,"/logout").permitAll()
.anyRequest()
.authenticated()
);
http.csrf(AbstractHttpConfigurer::disable);
http.sessionManagement((session)->session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(new JwtAuthenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);
http.formLogin((e)->{
e.loginProcessingUrl("/login")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler());
});
http.cors(Customizer.withDefaults());//跨域拦截关闭
return http.build();
}
/**
* 对密码进行BCrypt加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
Authorization
针对方法进行授权
steps:
对@Configuration的类添加注解
@EnableMethodSecurity
在方法前添加上
@PreAuthorize("hasAuthority('ROLE_teacher')")
- 这是预授权(在访问前进行权限的判断)
- 且仅有ROLE_teacher权限的用户才能访问该方法
- 预授权使用最多
针对Url进行授权
在SecurityConfig类中
@Bean
protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize)->authorize
.requestMatchers("/student/**")
//只有具有student:add权限的才可以匹配url/student/**
.hasAnyAuthority("student:add")
.requestMatchers("/teacher/**")
//只有具有teacher:add或ROLE_teacher权限的才可以匹配url/teacher/**
.hasAnyAuthority("ROLE_teacher","teacher:add")
//其余任何请求都必须经过权限验证后才可以访问
.anyRequest()
.authenticated()
);
http.formLogin().permitAll();
return http.build();
}
跨域
SpringBoot中的跨域
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedOriginPatterns("*")
.allowedMethods("GET","POST","PUT","DELETE")
.allowedHeaders("*")
.exposedHeaders("*");
}
}
SpringSecurity中的跨域
http.cors(Customizer.withDefaults());//跨域拦截关闭
Vue中的跨域
msg:通过代理解决
server: {
proxy: {
'/api': {
target: 'http://localhost:8080', // 后端服务器的地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') // 将请求路径中的 '/api' 前缀移除
}
}
},
此后发送axios请求时,直接向“api/xxx”访问即可
异常处理
import com.yannqing.utils.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.io.IOException;
/**
* 全局异常处理器
*
* @author redpig
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 权限校验异常
*/
@ExceptionHandler(AccessDeniedException.class)
public Result handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址 {},权限校验失败 {}", requestURI, e.getMessage());
return Result.error("没有权限,请联系管理员授权");
}
/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址 {},不支持 {} 请求", requestURI, e.getMethod());
return Result.error(e.getMessage());
}
/**
* 拦截未知的运行时异常
*/
@ExceptionHandler(RuntimeException.class)
public void handleRuntimeException(RuntimeException e, HttpServletRequest request, HttpServletResponse response) throws IOException {
String requestURI = request.getRequestURI();
log.error("请求地址 {},异常: {}", requestURI, e);
response.setContentType("application/json;charset=UTF-8");
response.setStatus(500);
response.getWriter().write(Result.errorJSON(e.getMessage()));
}
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址 {},发生系统异常.", requestURI, e);
return Result.error(e.getMessage());
}
}
集成thymleaf
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置yml
spring:
thymeleaf:
cache: false #开发时可以不使用缓存
check-template: true #是否检查模板
添加模板
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>
新建LoginController和IndexController
LoginController
@Controller
@RequestMapping("/login")
public class LoginController{
@RequestMapping("/toLogin")
public String toLogin(){
//返回thymeleaf逻辑视图名。物理视图=前缀+逻辑视图名+后缀
//其中,默认的前缀为:classpath:/templates/ ---即位于resources下面的templates
//默认的后缀是.html
return "login";
}
}
IndexController
@Controller
@Slf4j
public class IndexController{
@RequestMapping("/toIndex")
public String toIndex(){
return "main";//相当于跳转main.html
}
}
自定义登录页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>
登录页面
</h2>
<form action="/login/doLogin" method="post">
<table>
<tr>
<td>用户名</td>
<td><input type="text" name="uname" value="tomas"></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" name="pwd"></td>
<span th:if="${param.error}">用户名或密码错误</span>
</tr>
<tr> <td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
创建main页面
tips:基于thymeleaf
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系统首页</title>
</head>
<body>
<h1 align="center">系统首页</h1>
<a href="/student/query">查询学生</a>
<br>
<a href="/student/add">添加学生</a>
<br>
<a href="/student/update">更新学生</a>
<br>
<a href="/student/delete">删除学生</a>
<br>
<a href="/student/export">导出学生</a>
<br>
<br><br><br>
<h2><a href="/logout">退出</a></h2>
<br>
</body>
</html>
配置SecurityConfig
@Bean
protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize)->authorize
//任何请求都必须经过权限验证后才可以访问
.anyRequest()
.authenticated()
);
//关闭session
http.sessionManagement((session)->session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
//关闭跨域请求保护
http.csrf(AbstractHttpConfigurer::disable);
//配置表单登录
http.formLogin((formLogin)->formLogin
.loginPage("/toLogin")
.usernameParameter("uname")
.passwordParameter("pwd")
.loginProcessingUrl("/login/doLogin") //单击登录后进入url
.failureForwardUrl("/toLogin")//登录失败,回到登录页面
.successForwardUrl("/toIndex")//登录成功
.permitAll();
);
http.logout((logout)->logout
.logoutSuccessUrl("/toLogin") //退出登录后,返回到登录页面
);
return http.build();
}
创建student和teacher的页面
tips:在template文件夹下面创建student和teacher文件夹,然后在文件夹中分别创建各自的增删查改测试页面
格式基本一致
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">
系统首页-学生管理-新增
</h1>
<a href="/index/toIndex">返回</a>
</body>
</html>
创建403页面
tips:在resources/static/error文件夹下面创建403.html
编写html代码即可
调整Controller层
tips:将StudentController中返回json改成返回页面,分别对应student中的每一个页面
用户无权限时,不展示对应的按钮
- 添加依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecutiry5</artifactId>
</dependency>
- 修改main.html
在html标签中,添加xmlns:sec=“http://www.thymeleaf.org/extras/spring-security”
- 在对应按钮那边,添加上
sec:authorize="hasAuthority('student:query')"
即可
即<a href="/student/query" sec:authorize="hasAuthority('student:query')">查询学生</a>
拥有student:query权限的角色才可以看到这个按钮,否则无法看到
集成图片验证码
- 添加依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
- 建立Controller
@Controller
@Slf4j
public class CaptchaController {
@GetMapping("/code/getCaptchaCode")
public void getCaptchaCode(HttpServletRequest request, HttpServletResponse response) throws IOException{
CircleCaptcha circleCaptchaCaptchaUtil.createCircleCaptcha(200,100,2,20);//宽度,高度,字符数,圆的个数
String code = circleCaptcha.getCode();
log.info("生成的图片验证码为:"+code);
//将验证码存储到session中
request.getSession().setAttribute("CAPTCHA_CODE",code);
//将图片写入到响应流里,参数:图片,图片格式,响应流
a ImageIo.write(circleCaptcha.getImage,"jpeg",response.getOutputStream());
}
}
- 在授权请求中,将获取验证码放开。
http.authorizeHttpRequests((authorize)->authorize
.requestMatchers("/code/getCaptchaCode")
.permitAll()
.anyRequest()
.authenticated()
);
- 后面验证jwt好像也需要放开,需要自行测试
- 前端页面中使用验证码
<img src="/code/getCaptchaCode" onclick="this.src=this.src">//后面的意思是点击刷新
- 自定义过滤器,判断验证码
@Component
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse reponse, FilterChain filterChain){
//判断路径,是否是/login/doLogin
String requestURI = request.getRequestURI();
if(!requestURI.equals("/login/doLogin")){
doFilter(request,response,filterChain);//不是登录请求,直接放行
return;
}
//校验验证码
validateCode(request,response,filterChain);
}
public void validateCode(HttpServletRequest request, HttpServletResponse reponse, FilterChain filterChain){
//前端获取用户输入的code
String enterCode = request.getParameter("code")//输入框的名称,code
String captchaCodeInSession = request.getSession().getAttribute("CAPTCHA_CODE");
request.getSession().removeAttribute("captcha_code_error");//清除提示信息
if(enterCode==null) {
request.getSession.setAttribute("captcha_code_error","请您输入验证码");
response.sendRedirect("/toLogin")
return ;
}
if(captchaCodeInSession==null) {
request.getSession.setAttribute("captcha_code_error","验证码错误");
return ;
}
if(!enterCode.equalsIgnoreCase(captchaCodeInSession)) {
request.getSession.setAttribute("captcha_code_error","验证码输入错误");
response.sendRedirect("/toLogin");
return;
}
request.getSession().removeAttribute("CAPTCHA_CODE");//移除验证码
//程序执行到这里,说明验证码正确
this.doFilter(request,response,filterChain);
}
}
tips: 在前端页面中错误信息如下写
<span th:text="${session.captcha_code_error}">username</span>
- 放到过滤器链中。
@Resource
private ValidateCodeFilter validateCodeFilter;
@Bean
protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//在用户名密码认证过滤器前添加图片验证码过滤器
http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFIlter.class)
}
jwt认证
jwt的基本信息
- 全名:json web token
- 包含header,payload,signature三部分
- 通过“.”连接
header
{
"alg": "HS256", //签名算法
"typ": "JWT" //类型,通常默认为JWT
}
然后,对该 JSON 进行Base64Url编码以形成 JWT 的第一部分。
payload
msg:通常有三部分声明(注册声明,公共声明,私人声明)
注册声明(预定义的)
{
"iss(issuer)":"签发人",
"exp(expiration time)":"过期时间",
"sub(subject)":"主题",
"aud(audience)":"受众",
"nbf(Not Before)":"生效时间",
"iat(Issued At)":"签发时间",
"jti(JWT ID)":"编号"
}
私人声明(自定义)
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后对payload进行Base64Url编码以形成 JSON Web 令牌的第二部分。
关闭session
//关闭session
http.sessionManagement((session)->session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
添加依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.3.0</version>
</dependency>
编写jwt工具类
action:将常用的jwt方法例如创建jwt,校验jwt等封装为工具类
工具类
message:
两种方法:
- 将用户信息以及权限封装为jwt存储,并写出对应的获取用户信息以及权限的方法
- 此方法需要将用户信息和权限信息封装为UsernamePasswordAuthenticationToken并传给安全上下文才能通过用户名密码过滤器
- 将用户认证信息Authentication直接封装为token
- 此方法需要将Authentication给解析后赋值给自定义的JwtAuthentication(实现Authentication接口),并将其传给安全上下文才可以
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Date;
import java.util.List;
@Slf4j
public class JwtUtils {
private static final String secret = "111111";
/**
* 根据认证信息Authentication生成JWT token
*/
public static String token(Authentication authentication){
return JWT.create()
.withExpiresAt(new Date(System.currentTimeMillis()+ 1000L * 60 * 60 * 24 * 30)) //设置过期时间:单位毫秒
.withAudience(JSON.toJSONString(authentication)) //设置接受方信息,一般时登录用户
.sign(Algorithm.HMAC256(secret));
}
/**
* 根据用户详细信息,权限信息 生成token
* @param userInfo 用户详细信息,密码为空
* @param authList 用户权限信息
* @return
*/
public static String token(String userInfo, List<String> authList){
return JWT.create()
.withExpiresAt(new Date(System.currentTimeMillis()+ 1000L * 60 * 60 * 24 * 30)) //设置过期时间:单位毫秒
.withClaim("userInfo",userInfo)
.withClaim("authList",authList)
.sign(Algorithm.HMAC256(secret));
}
/**
* 根据指定日期返回token
* @param authentication 认证信息
* @param time 过期时间 单位毫秒
* @return 返回token
*/
public static String token(Authentication authentication,Long time){
return JWT.create()
.withExpiresAt(new Date(System.currentTimeMillis()+ 1000L * 60 * 60 * 24 * 30)) //设置过期时间:单位毫秒
.withAudience(JSON.toJSONString(authentication)) //设置接受方信息,一般时登录用户
.sign(Algorithm.HMAC256(secret));
}
/**
* 验证token合法性
*/
public static void tokenVerify(String token){
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
jwtVerifier.verify(token);//没报错说明验证成功
log.info("token校验成功!");
// JWT.decode(token).getExpiresAt();
// String json = JWT.decode(token).getAudience().get(0);
// JwtAuthentication jwtAuthentication = JSON.parseObject(json, JwtAuthentication.class);
// SecurityContextHolder.getContext().setAuthentication(jwtAuthentication);
}
/**
* 刷新token
* @param token
* @return
*/
public static String refreshToken(String token){
JwtUtils.tokenVerify(token);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return JwtUtils.token(authentication);
}
public static String getUserInfoFromToken(String token){
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return decodedJWT.getClaim("userInfo").asString();
}
public static List<String> getUserAuthorizationFromToken(String token){
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return decodedJWT.getClaim("authList").asList(String.class);
} catch (Exception e) {
return null;
}
}
}
JwtAuthentication
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Collection;
/**
* {@link Authentication}及其实现类的大部分属性没有提供setter方法,
* 所以在通过json转换回Authentication时,没有setter方法的属性就赋值为空,此类是json转换回Authentication的中间类。
*/
public class JwtAuthentication implements Authentication {
private Collection<SimpleGrantedAuthority> authorities;
private Object details;
private boolean authenticated;
private Object principal;
private Object credentials;
@Override
public Collection<SimpleGrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(Collection<SimpleGrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public Object getDetails() {
return details;
}
public void setDetails(Object details) {
this.details = details;
}
@Override
public boolean isAuthenticated() {
return authenticated;
}
@Override
public void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
}
@Override
public Object getPrincipal() {
return principal;
}
public void setPrincipal(Object principal) {
this.principal = principal;
}
@Override
public Object getCredentials() {
return credentials;
}
public void setCredentials(Object credentials) {
this.credentials = credentials;
}
@Override
public String getName() {
return null;
}
}
认证成功处理器
action:当用户登录成功后,此认证成功处理器将自动生成jwt,后面就可以带着token访问其他页面了
编写处理器
import com.yannqing.utils.JwtUtils;
import com.yannqing.utils.Result;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//生成token,这是其中之一的方法
String token = JwtUtils.token(authentication);
//存入redis
Jedis jedis = new Jedis("http://127.0.0.1:6379/");
jedis.auth("123456");
jedis.set("token:"+token, String.valueOf(authentication),new SetParams().ex(60*60*2));
//返回给前端
Map<String, Object> data = new HashMap<>();
data.put("token",token);
response.getWriter().write(Result.okJSON(data));
}
}
将认证成功处理器交给spring security
//配置表单登录
http.formLogin((formLogin)->formLogin
//配置认证成功处理器
.successHandler(new LoginSuccessHandler())
.permitAll()
);
认证失败处理器
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import java.io.IOException;
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("loginErr");
System.out.println("登录异常信息:");
exception.printStackTrace();
}
}
将认证失败处理器交给spring security
//配置表单登录
http.formLogin((formLogin)->formLogin
//配置认证成功处理器
.successHandler(new LoginFailureHandler())
.permitAll()
);
编写jwt校验过滤器
action:每次访问非登录页面的时候,需要校验一下用户携带的jwt是否合法,是否有权限访问等等
编写过滤器
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yannqing.entity.User;
import com.yannqing.utils.JwtUtils;
import com.yannqing.utils.Result;
import com.yannqing.utils.YannqingTools;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import redis.clients.jedis.Jedis;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
/**
* 登录之前获取token校验:如果有token、再去校验token的合法性:如果没有报错 则认为登录成功
* 【token是根据SpringSecurity的Authentication生成的,相当于token就是SpringSecurity认证后的凭证】
*/
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
//匿名地址直接访问
if(YannqingTools.contains(requestURI)){
filterChain.doFilter(request, response);
return;
}
//获取JWT
String token = request.getHeader("token");
log.info("接收到的token: {}",token);
Jedis jedis = new Jedis("http://127.0.0.1:6379/");
jedis.auth("123456");
//判断redis中是否存在jwt
String redisToken = jedis.get("token:" + token);
if (redisToken==null){
response.setStatus(500);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONUtil.toJsonStr(Result.error("您已退出,请重新登录!")));
return;
}
if (token != null) {
try {
JwtUtils.tokenVerify(token);
//下面是第二种方法生成的token,需要用户详细信息与用户权限信息
// String userInfo = JwtUtils.getUserInfoFromToken(token);
// ObjectMapper objectMapper = new ObjectMapper();
// User user = objectMapper.readValue(userInfo, User.class);
// List<String> authList = JwtUtils.getUserAuthorizationFromToken(token);
// List<SimpleGrantedAuthority> authorities = authList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
// UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user,null,authorities);
// SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}catch (Exception e){
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONUtil.toJsonStr(Result.error("非法token")));
return;
}
}
filterChain.doFilter(request, response);
}
}
将过滤器器交给spring security
//添加jwt校验过滤器到用户名密码过滤器之前
http.addFilterBefore(JwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
与redis结合使用
action:由于当用户每次登陆时都会创建一个新的jwt,所以退出登录后,原来的jwt不能自动销毁,可能会被黑客利用,所以通过将jwt放到redis中,每次退出登录时,删除redis中的jwt,每次访问url的时候,判断redis中的jwt是否存在。
关闭csrf跨域请求保护
//关闭跨域请求保护
http.csrf(AbstractHttpConfigurer::disable);
添加依赖
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
生成jwt时存入redis
action:首先需要依赖注入StringRedisTemplate,然后在认证成功处理器中,生成jwt后添加下面的代码
//将jwt放到redis中,过期时间和jwt过期时间一致
stringRedisTemplate.opsForValue().set("loginToken:"+jwt,objectMapper.writeValueAsString(authentication),2, TimeUnit.HOURS);
退出成功处理器
action:当用户退出时,将jwt从redis中删除
编写处理器
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yannqing.util.JwtUtils;
import com.yannqing.vo.Result;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.PrintWriter;
@Component
@Slf4j
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private JwtUtils jwtUtils;
@Resource
private ObjectMapper objectMapper;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//取出请求头中的Authorization
String authorization = request.getHeader("Authorization");
if (authentication==null){
Result result = new Result(0,"jwt不存在",null);
printToken(request,response,result);
return;
}
//从Authorization中取出jwt并校验
String jwtToken = authorization.replace("bearer ", "");
boolean verifyResult = jwtUtils.verifyToken(jwtToken);
if (!verifyResult){
Result result = new Result(0,"jwt非法",null);
printToken(request,response,result);
return;
}
//从redis中删除登录时候的jwtToken
stringRedisTemplate.delete("loginToken:"+jwtToken);
Result result = new Result(1,"jwt删除成功",null);
printToken(request,response,result);
}
/**
* 将token响应给前端
* @param request
* @param response
* @param result
* @throws IOException
*/
private void printToken(HttpServletRequest request, HttpServletResponse response, Result result) throws IOException {
String strResponse = objectMapper.writeValueAsString(result);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(strResponse);
writer.flush();
}
}
配置到spring security
action:先依赖注入处理器
//配置退出成功处理器
http.logout((logout)->logout
.logoutSuccessHandler(myLogoutSuccessHandler)
);
校验jwt时,查询redis
action:每次通过jwt校验过滤器时,需要查询redis中是否存在。
位置放在jwt校验过滤器中的,校验jwt后
//判断redis中是否存在jwt
String redisToken = stringRedisTemplate.opsForValue().get("loginToken:" + jwtToken);
if (redisToken==null){
Result result = new Result(0,"您已退出,请重新登录",null);
printToken(request,response,result);
return;
}