1.认证授权的概述
1.1什么是认证?
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条,抖音等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,==输入账号和密码登录微信的过程就是认证==。
系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法,方可访问该系统的资源。
认证︰用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统==资源==时系统要求验证用户的身份信息,身份合法 方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:==用户名密码登录,二维码登录,手机短信登录,指纹认证等方式==。
1.2什么是会话?
用户认证通过后,为了避免用户的每次操作都进行认证,可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。
1.2.1基于session的认证
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发送给客户端的session_id存放到cookie中,这样用户客户端请求时带上session_id就可以验证服务器端是否存在session数据,以此完成瀛湖的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
1.2.2基于Token的认证
它的交互流程是,用户认证成功后,服务端生成一个token【令牌】(唯一字符串)【uuid,jwt】发送给客户端,客户端可以放在token通过验证后即可确认用户身份。
基于session的认证方式,由servlet规范制定,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式,则一般不需要服务端存储token,并且不限制客户端的存储方式并且不限制客户端的存储方式cookie sessionStorage LocalStorage Vuex。如今移动互联网时代更多类型的客户端[pC ,android,IOS,]需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
- 使用前后端分离或后台使用了集群--一定采用token模式。--一般线上的都是这种
- 传统的项目前端和后端都在一个工程下--基于session模式。--公司的系统(只需要很少的人登录)、仓库管理。。。
1.3什么是授权
还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的==权限==才可以正常使用发送红包==功能==,拥有发朋友圈功能的权限才可以便用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
1.权限【权限表】--资源【接口】
(一期项目的权限管理--不同的角色有不同的功能)
1.3.1为什么要授权
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,==授权是在认证通过后发生的==,控制不同的用户能够访问不同的资源。授权:授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
认证授权的框架:
[1]shiro 轻量级的认证授权 它可以整合任意框架 它支持javase和javaee
[2]springsecurity 重量级的认证授权框架。它只能和spring整合,只支持javaee web框架。
spring非常麻烦,但是现在和springboot整合就很简单了。
3.概述springsecurity
3.1什么是SpringSecurity?
百度百科:Spring Security是一个能够为基于Spring的企业应用系统提供==声明式的安全访问控制解决方案的安全框架==。它提供了一组可以在Sprirg应用上下文中配置的Bean,充分利用了Spring IOC,DI(控制反转Inversion of Control ,DI:Dependency Injection依赖主入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。以上解释来源于百度白科。
可以一句话来概括,SpringSecurity 是一个安全框架。可以帮我们完成认证,密码加密,授权,rememberme的功能。
4.快速入门SpringSecurity
1.导入依赖--也可以在创建项目时就选中添加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.创建接口资源----controller层
package com.wjy.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
3.访问启动项目并访问资源:http://localhost:8080/hello
发现 帮你跳转到登录页面。 因为springsecurity包含了很多过滤器,认证过滤器发现你没有登录就访问资源。默认调整到它内置的登录页面
4.1自定义账号和密码
4.1.1设置多个用户--定义一个配置类--基于内存
package com.wjy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
//如果springboot的版本2.7.0以上的会有过期的波浪线
public class MySercurityConfig extends WebSecurityConfigurerAdapter {
//密码编码器--会自动加密
@Bean
public PasswordEncoder passwordEncoder() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
//内存中认证和授权
.inMemoryAuthentication()
//用户名
.withUser("wjy")
//密码---加密和密码编辑器保持一致
.password(passwordEncoder().encode("123456"))
//角色
.roles("admin")
//具有的权限
.authorities("admin:select", "admin:update","admin:insert")
//多个用户之间用and连接
.and()
.withUser("wjy1")
.password(passwordEncoder().encode("123456"))
.roles("admin");
}
}
此时登录会报错,使用密码加密器。
修改配置类
4.1.2密码加密器
分成两种类型:对称加密和非对称加密
对称加密:表示加密和解密使用同一把密钥。
非对称加密:表示加密和解密不是使用同一把密钥。md5 hash
package com.wjy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public class Test {
public static void main(String[] args) {
PasswordEncoder passwordEncoder=new BCryptPasswordEncoder();
//用于加密
String encode = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
String encode3 = passwordEncoder.encode("123456");
System.out.println(encode);
System.out.println(encode2);
System.out.println(encode3);
//安全.
boolean matches = passwordEncoder.matches("123456", encode2);
System.out.println("是否密码正确:"+matches);
}
}
加密后无法解密--通过hash随机生成的--不用解密会自动比对
使用new BCryptPasswordEncoder()对象的encode()方法进行加密--安全的
MD5每次生成的加密都会相同--安全性低
4.1.3上面测试的完整代码流程
package com.wjy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
//如果springboot的版本2.7.0以上有其他的写法
public class MySercurityConfig extends WebSecurityConfigurerAdapter {
//密码编码器--会自动加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//用户认证和授权
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
//内存中认证和授权
.inMemoryAuthentication()
//用户名
.withUser("wjy")
//密码---加密和密码编辑器保持一致
.password(passwordEncoder().encode("123456"))
//角色
.roles("admin")
//具有的权限
.authorities("admin:select", "admin:update","admin:insert")
.and()
.withUser("wjy1")
.password(passwordEncoder().encode("123456"))
.roles("admin");
}
//登录前后的权限设置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
//登录页面
.loginPage("/login.html")
//登录成功后跳转的页面
.loginProcessingUrl("/login")
//登录成功后跳转的页面--必须都是Post请求
.successForwardUrl("/success")
//登录失败后跳转的页面
.failureForwardUrl("/error")
//上面的页面请求无需认证--无需权限认证特权
.permitAll();
//如果使用的登录页面不是自己的--禁止跨越伪造请求的过滤器
http.csrf().disable();
//其他所有请求都需要认证
http.authorizeRequests()
.anyRequest()
.authenticated();
}
}
package com.wjy.controller.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity//开启Spring Security的功能
public class SecurityConfig {
//加密器
@Bean
public PasswordEncoder passwordEncoder(){
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder;
}
//设置用户
@Bean
public UserDetailsService myconfigure() throws Exception{
return new InMemoryUserDetailsManager(
User.withUsername("wjy")
.password(passwordEncoder().encode("123456"))
.roles("admin")
.authorities("user:list","user:add")
.build()
);
}
//登录前后的权限设置
@Bean
public SecurityFilterChain myconfigure2(HttpSecurity http)throws Exception{
http.formLogin(
//登录页面
form->form.loginPage("/login.html")
//要处理请求的路径
//登录处理
.loginProcessingUrl("/login")
.successForwardUrl("/success")
.permitAll()
);
//如果使用的登录页面不是自己的--禁止跨越伪造请求的过滤器
http.csrf(a->a.disable());
//其他的所有请求都需要认证
http.authorizeHttpRequests(
auth->auth.anyRequest()
.authenticated()
);
return http.build();
}
}
package com.wjy.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
//@RestController//-->返回字符串
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@PostMapping("/success")
public String success(){
return "redirect:success.html";
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
h1 {
text-align: center;
}
form {
background-color: #ffffff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
input[type="text"], input[type="password"] {
width: 93%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 3px;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #007BFF;
color: #ffffff;
border: none;
border-radius: 3px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<form action="/login" method="post">
<h1>账号登录</h1>
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<input type="submit" value="登录">
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>成功登录后跳转的页面</title>
</head>
<body>
成功登录
</body>
</html>
4.2获取当前登录者的信息(重要)
SpringSecurity默认把当前用户的信息保存在SecurityContext上下文中了--如同之前的session
package com.wjy.config;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/info")
public Authentication info() {
//1. 获取SecurityContext对象
SecurityContext context = SecurityContextHolder.getContext();
//2.把用户得到信息封装到Authentication对象中(包含了用户名--角色以及权限--状态(是否过期到期了))
Authentication authentication = context.getAuthentication();
//3.获取所有的信息--getPrincipal()
UserDetails principal = (UserDetails) authentication.getPrincipal();
//4.获取用户名--getUsername()具体的信息
System.out.println(principal.getUsername());
return authentication;
}
}
5.security完成授权
授权:把当前用户具有的权限 和对应的资源 绑定的过程。
-----授权一定发生在认证后
定义一些资源接口
@GetMapping("select")
public String select(){
System.out.println("查询用户");
return "查询用户";
}
@GetMapping("insert")
public String insert(){
System.out.println("添加用户");
return "添加用户";
}
@GetMapping("update")
public String update(){
System.out.println("修改用户");
return "修改用户";
}
@GetMapping("delete")
public String delete(){
System.out.println("删除用户");
return "删除用户";
}
@GetMapping("export")
public String export(){
System.out.println("导出用户");
return "导出用户";
}
修改配置类
2.7版本之前的
2.7版本之后的
5.1使用注解完成授权
上面的授权代码比较麻烦,我们可以使用注解完成授权的过程。
1.开启security授权的注解驱动
2.7版本之前的
2.7版本之后的
2.在资源上使用注解
@GetMapping("/select")
@PreAuthorize("hasAnyAuthority('user:select')")
public String select(){
return "select";
}
5.2权限不足跳转指定页面
修改配置类
6.【源码分析】SpringSecurity认证授权(了解)
为什么需要分析认证得流程?
我们要通过数据库进行认证和授权。
6.1结构总览
Spring security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AoP等技术来实现,SpringSecurity对web资源的保护是靠==Filter=='实现的,所以从这个Filter来入手,逐步深入Spring Security 原理。当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的 Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是 Spring Security过虑器链结构图:
6.2过滤器中主要的几个过滤器及其作用
1.SecurityContextPersistenceFi1ter
这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepesitory 中获取SecurityContext,然后把它设置给securityContextHolder.在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository ,同时清除securityContextHolder所持有的SecurityContext;
2. UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和AuthenticationFailureHandler,这些都可以根据需求做相关改变;
3. FilterSecurityInterceptor
是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问;
4 .ExceptionTranslationFilter
能够捕获来自Filterchain所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException和 AccessDeniedException,其它的异常它会继续抛出。
6.3【源码分析】Spring security 认证工作流程
UsernamePasswordAuthenticationFilter (attemptAuthentication)
ProviderManager (authenticate)DaoAuthenticationProvider (retrieveUser)AbstractUserDetailsAuthenticationProvider (authenticate)
1 认证流程图
查询数据库进行比对。
查询用户信息的代码--->UserDetailsService中loadUserByUsername该方法。 我们只需要重写该方法即可。
7. 自定义UserDetailsService接口类
1.自定义service
package com.wjy.service;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.Collection;
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//自己的业务--表示数据库中存在该用户--获取到权限
if ("wjy".equals(username)){
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
//从数据库中可以获取用户的权限
authorities.add(new SimpleGrantedAuthority("user:select"));
authorities.add(new SimpleGrantedAuthority("user:update"));
authorities.add(new SimpleGrantedAuthority("user:insert"));
return new User("wjy","$2a$10$NZ.q7Y.fJJ/5YxvqYXJ1Auq04JYxrZ7YQ5/x3XKZ7YQ5/x3XKZ7YQ5",authorities);
}else if ("wjy1".equals(username)){
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("user:delete"));
return new User("wjy1","$2a$10$NZ.q7Y.fJJ/5YxvqYXJ1Auq04JYxrZ7YQ5/x3XKZ7YQ5/x3XKZ7YQ5",authorities);
}
return null;
}
}
2.修改配置类
2.7之前的版本
2.7之后的版本
8.SpringSecurity整合thymeleaf
模板引擎等价于jsp.
/*
Navicat Premium Data Transfer
Source Server : gz02
Source Server Type : MySQL
Source Server Version : 80032
Source Host : localhost:3306
Source Schema : security
Target Server Type : MySQL
Target Server Version : 80032
File Encoding : 65001
Date: 24/11/2023 11:16:37
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`perid` int(0) NOT NULL AUTO_INCREMENT,
`pername` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
`percode` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`perid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, '用户查询', 'user:query');
INSERT INTO `sys_permission` VALUES (2, '用户添加', 'user:add');
INSERT INTO `sys_permission` VALUES (3, '用户修改', 'user:update');
INSERT INTO `sys_permission` VALUES (4, '用户删除', 'user:delete');
INSERT INTO `sys_permission` VALUES (5, '用户导出', 'user:export');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`roleid` int(0) NOT NULL AUTO_INCREMENT,
`rolename` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`roleid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '管理员');
INSERT INTO `sys_role` VALUES (2, '测试人员');
INSERT INTO `sys_role` VALUES (3, '普通用户');
-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`perid` int(0) NULL DEFAULT NULL,
`roleid` int(0) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES (2, 1);
INSERT INTO `sys_role_permission` VALUES (1, 1);
INSERT INTO `sys_role_permission` VALUES (3, 1);
INSERT INTO `sys_role_permission` VALUES (4, 1);
INSERT INTO `sys_role_permission` VALUES (2, 2);
INSERT INTO `sys_role_permission` VALUES (1, 2);
INSERT INTO `sys_role_permission` VALUES (3, 2);
INSERT INTO `sys_role_permission` VALUES (1, 3);
INSERT INTO `sys_role_permission` VALUES (5, 3);
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`userid` int(0) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
`userpwd` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
`sex` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
`address` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`userid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, '张三', '$2a$10$cI7e7bgSs9.9nNHhxKO9LuK/Ll.AeZwgUyZb77oD2y3UwwZyZhWG6', '男', '郑州');
INSERT INTO `sys_user` VALUES (2, '李四', '$2a$10$cI7e7bgSs9.9nNHhxKO9LuK/Ll.AeZwgUyZb77oD2y3UwwZyZhWG6', '男', '北京');
INSERT INTO `sys_user` VALUES (3, '王五', '$2a$10$cI7e7bgSs9.9nNHhxKO9LuK/Ll.AeZwgUyZb77oD2y3UwwZyZhWG6', '女', '杭州');
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`userid` int(0) NOT NULL,
`roleid` int(0) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);
INSERT INTO `sys_user_role` VALUES (2, 2);
INSERT INTO `sys_user_role` VALUES (3, 3);
INSERT INTO `sys_user_role` VALUES (1, 2);
SET FOREIGN_KEY_CHECKS = 1;
pom依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
spring.application.name=spring_security
spring.datasource.url=jdbc:mysql://localhost:3306/tb_security?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis-plus.mapper-locations=classpath*:mapper/*.xml
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
实体类用快速生成--加上mybatis-plus
生成实体类、mapper操作数据库、mapper.xml映射文件
mapper--需要自己定义的数据库查询
(根据数据库中已经存在的用户名匹配对应的密码判断是否登录成功)
public interface PermissionMapper extends BaseMapper<Permission> {
public List<Permission> selectByUserId(Integer userId);
}
serivce类
条件查找,判断用户是否存在:
存在--查询具有的权限(链表条件查询--自定义)
package com.wjy.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wjy.entity.User;
import com.wjy.mapper.PermissionMapper;
import com.wjy.mapper.UserMapper;
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.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private PermissionMapper permissionMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息--username必须唯一
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
User user = userMapper.selectOne(wrapper);
//判断用户是否存在
if (Objects.nonNull(user)){
//查询当前用户具有的权限
List<SimpleGrantedAuthority> collection = permissionMapper.selectByUserId(
user.getUserid())//根据用户id查询权限
.stream()//转换流
.map(p -> new SimpleGrantedAuthority(p.getPercode()))//把每次查询的权限码封装成SimpleGrantedAuthority
.collect(Collectors.toList());//转换为集合
return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getUserpwd(),collection);
}
return null;
}
}
配置类
package com.wjy.config;
import com.wjy.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
//如果springboot的版本2.7.0以上有其他的写法
public class MySercurityConfig extends WebSecurityConfigurerAdapter {
//密码编码器--会自动加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private MyUserDetailService myUserDetailService;
//用户认证和授权
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService);
}
//登录前后的权限设置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
//登录页面
.loginPage("/login.html")
//登录成功后跳转的页面
.loginProcessingUrl("/login")
//登录成功后跳转的页面--必须都是Post请求
.successForwardUrl("/success")
//登录失败后跳转的页面
.failureForwardUrl("/error")
//上面的页面请求无需认证--无需权限认证特权
.permitAll();
//权限不足跳转的页面
http.exceptionHandling().accessDeniedPage("/error.html");
//如果使用的登录页面不是自己的--禁止跨越伪造请求的过滤器
http.csrf().disable();
//其他所有请求都需要认证
http.authorizeRequests()
.anyRequest()
.authenticated();
}
}
controller控制层
package com.wjy.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/info")
public Authentication info() {
//1. 获取SecurityContext对象
SecurityContext context = SecurityContextHolder.getContext();
//2.把用户得到信息封装到Authentication对象中(包含了用户名--角色以及权限--状态(是否过期到期了))
Authentication authentication = context.getAuthentication();
//3.获取所有的信息--getPrincipal()
UserDetails principal = (UserDetails) authentication.getPrincipal();
//4.获取用户名--getUsername()具体的信息
System.out.println(principal.getUsername());
return authentication;
}
@GetMapping("/select")
@PreAuthorize("hasAnyAuthority('user:query')")
public String select(){
return "select";
}
@GetMapping("/insert")
@PreAuthorize("hasAnyAuthority('user:add')")
public String insert(){
return "insert";
}
@GetMapping("/update")
@PreAuthorize("hasAnyAuthority('user:update')")
public String update(){
return "update";
}
@GetMapping("/delete")
@PreAuthorize("hasAnyAuthority('user:delete')")
public String delete(){
return "delete";
}
@GetMapping("/export")
@PreAuthorize("hasAnyAuthority('user:export')")
public String export(){
return "export";
}
}
package com.wjy.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
//@RestController//-->返回字符串
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@PostMapping("/success")
public String success(){
return "success";//这里不能是redirect重定向
}
}
跳转的权限网页
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
>
<head>
<meta charset="UTF-8">
<title>成功登录后跳转的页面</title>
</head>
<body>
<a href="user/select" sec:authorize="hasAuthority('user:query')">查询用户</a><br>
<a href="user/delete" sec:authorize="hasAuthority('user:delete')">删除用户</a><br>
<a href="user/insert" sec:authorize="hasAuthority('user:add')">添加用户</a><br>
<a href="user/update" sec:authorize="hasAuthority('user:update')">修改用户</a><br>
<a href="user/export" sec:authorize="hasAuthority('user:export')">导出用户</a>
</body>
</html>
入口函数
9.SpringSecurity完成前后端完全分离
前后端的分离:响应的数据 必须为JSON数据。
需要修改的代码有哪些?
1.登录成功需要返回json数据
2.登录失败需要返回json数据
3.权限不足时返回json数据
4.未登录访问资源返回json数据
9.1JWT的概述
1.什么是Jwt
Json web token (JWT),是为了在网络应用环境间传递声明 而执行的一种基于JSON的开放标准((RFC7519).该token被设计为紧凑且==安全==的,特别适用于==分布式站点的单点登录(SSO)场景==。
JWT的声明一般被用来在身份提供者 和服务提供者 间传递被认证 的用户身份信息 ,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
官网: JSON Web Token Introduction - jwt.io
2 前后端完全分离认证问题
互联网服务离不开用户认证。一般流程是下面这样。
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。3、服务器向用户返回一个session_id,写入用户的Cookie。
4、用户随后的每一次请求,都会通过Cookie,将session_id传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是前后端分离的服务导向架构,就要求session 数据共享,每台服务器都能够读取session。
举例来说,A网站和B网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在==客户端==,每次请求都发回服务器。JWT就是这种方案的一个代表。
JWT: 影响了网络带宽。
3.JWT的原理
JWT的原理是,服务器认证以后,生成一个==JSON对象==,发回给用户,就像下面这样。
{
"姓名":"张三",
"角色":"管理员",
"到期时间":"2022年8月1日0点0分"
}
以后,用户与服务端通信的时候,都要发回这个JSON对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
4.JWT的数据结构
它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT内部是没有换行的,这里只是为了便于展示,将它写成了几行。JWT的三个部分:
Header (头部)
Payload(负载 载荷)
Signature(签名)
写成一行,就是下面的样子。
Header.Payload.Signature
5.Header
Header 部分是一个JSON对象,描述JWT的元数据,通常是下面的样子。
上面代码中,
alg属性 表示签名的算法( algorithm),默认是 HMAC SHA256 (写成 HS256) ;
typ属性 表示这个令牌(token)的类型 (type), JWT令牌统一写为JWT。
最后,将上面的JSON对象使用Base64URL算法转成字符串。
6.Payload
Payload 部分也是一个JSON对象,==用来存放实际需要传递的数据==。JWT规定了7个官方字段,供选用。iss (issuer):签发人exp (expiration time):过期时间
sub (subject):主题 aud (audience):受众
nbf (Not Before):生效时间
iat (lssued At):签发时间
jti (JWT ID):编号除了官方字段,你还可以在这个部分定义自己的字段,下面就是一个例子。
注意,JWT 默认 是不加密 的,任何人都可以读到,所以不要把 ==秘密信息【密码】 ==放在这个部分。这个JSON 对象也要使用Base64URL 算法转成字符串。
7.Sinature
Signature部分是对前两部分的签名 ,防止数据篡改。
首先,需要指定一个密钥(secret) 。这个密钥只有服务器才知道 ,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。HMACSHA256(base64UrlEncode(header) + ".""+base64UrlEncode(payload),secret)算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
9.2JWT的使用方式
客户端收到服务器返回的JWT,可以存储在Cookie里面,也可以存储在localStorage。SessionStorage此后,客户端 每次与 服务器 通信 ,都要带上这个 JWT。
把它放在Cookie里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP请求的头信息Authorization字段里面。
1.引入jar
<!--引入jwt的依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
2.创建jwt的工具类
①创建令牌---②校验令牌---③根据token获取自定义的信息
package com.wjy.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Verification;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtil {
private static final String SECRET = "wjyGyh";
//通过jwt创建token令牌
public static String createToken(Map<String,Object> map){
Map<String, Object> head = new HashMap<>();
head.put("alg", "HS256");
head.put("typ", "JWT");
Date date = new Date();//当前时间--发布时间
Calendar instance = Calendar.getInstance();//获取当前时间
instance.set(Calendar.SECOND,7200);//当前时间的基础上添加2个小时
Date time = instance.getTime();
return JWT.create()
.withHeader(head)//头部信息
.withIssuedAt(date)//发布日期
.withExpiresAt(time)//过期时间
.withClaim("userinfo",map)//存放用户信息--设置个人信息
.sign(Algorithm.HMAC256(SECRET));//签名
}
//校验token
public static boolean verify(String token){
Verification require = JWT.require(Algorithm.HMAC256(SECRET));
try {
require.build().verify(token);
return true;
} catch (JWTVerificationException e) {
System.out.println("token错误,校验失败");
return false;
}
}
//根据token获取自定义的信息
public static Map<String,Object> getTokenInfo(String token,String key){
return JWT.require(Algorithm.HMAC256(SECRET))
.build()
.verify(token)
.getClaim(key)
.asMap();
}
}
9.3登录成功需要返回json数据
第一种方案:基于redis(集群)--相当于之前的session共享(单例)
缺点:1.redis压力太大。2.项目依赖于第三方组件,存在redis第三方组件宕机后的影响。
第二种:基于JWT
JWT会生成唯一标识,而且校验唯一标识。信息会存放在JWT中,同时也可以从JWT中获取。
1.修改权限设置
2.书写登录成功后跳转的页面所携带给前端的数据
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.22</version>
</dependency>
//登录成功后跳转的页面--必须都是Post请求
private AuthenticationSuccessHandler successHandler() {
return (httpServletRequest, httpServletResponse, authentication)->{
//设置响应的编码
httpServletResponse.setContentType("application/json;charset=utf-8");
//获取输出对象
PrintWriter writer = httpServletResponse.getWriter();
//封装map数据
Map<String, Object> map = new HashMap<>();
//①存放用户名
map.put("username", authentication.getName());
//获取权限
List<String> collect = authentication.getAuthorities()
.stream()
.map(item -> item.getAuthority()).collect(Collectors.toList());
//②存放权限码
map.put("permissions", collect);
//1.生成token
String token = JWTUtil.createToken(map);
//2.放入token,返回json数据
R r = new R(200, "登录成功", token);
//转为json数据
String jsonString = JSON.toJSONString(r);
//给前端判断的依据
writer.println(jsonString);
//刷新流
writer.flush();
//关闭流
writer.close();
};
}
9.4登录失败返回json数据给前端
1.修改配置类
2.书写登录失败处理的函数
//登录失败后给前端返回的json数据
private AuthenticationFailureHandler failureHandler() {
return (httpServletRequest, httpServletResponse, e)->{
//设置响应的编码--->获取输出对象
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
R r = new R(500, "登录失败", e.getMessage());
//转为json数据-->给前端判断的依据-->刷新流-->关闭流
String jsonString = JSON.toJSONString(r);
writer.println(jsonString);
writer.flush();
writer.close();
};
}
9.5权限不足返回给前端的json数据
1.修改配置类
2.书写权限不足要处理的类
//权限不足后给前端返回的json数据
private AccessDeniedHandler accessDeniedHandler() {
return (httpServletRequest, httpServletResponse, e)->{
//设置响应的编码--->获取输出对象
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
//创建R对象 并转换成json数据给前端 判断作为的依据-->刷新流-->关闭流
R r = new R(403, "权限不足", e.getMessage());
String jsonString = JSON.toJSONString(r);
writer.println(jsonString);
writer.flush();
writer.close();
};
}
9.6未登录返回json数据
1.需要自定义一个过滤器
package com.wjy.filter;
import com.alibaba.fastjson.JSON;
import com.wjy.util.JWTUtil;
import com.wjy.vo.R;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component//交于spring容器管理
public class LoginFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
response.setContentType("application/json;charset=utf-8");
//获取请求路径
String path = request.getRequestURI();
//获取请求方式
String method = request.getMethod();
//判断请求路径是否为登录路径--是的话放行
if ("/login".equals(path) && "POST".equals(method)){
filterChain.doFilter(request,response);
return;
}
//1.获取token令牌--从请求头中获取
String token = request.getHeader("token");
//2.判断token是否为空
//①token为空
if (StringUtils.isEmpty(token)){
R r = new R(500, "未登录", null);
PrintWriter writer = response.getWriter();
String jsonString = JSON.toJSONString(r);
writer.write(jsonString);
writer.flush();
writer.close();
return;
}
//②token不为空--验证token失败的情况(token不正确)
if (!JWTUtil.verify(token)){
R r = new R(500, "token失效,登录失败", null);
PrintWriter writer = response.getWriter();
String jsonString = JSON.toJSONString(r);
writer.write(jsonString);
writer.flush();
writer.close();
return;
}
//③token不为空--验证token成功--获取token中的信息
//获取token中的信息
SecurityContext context = SecurityContextHolder.getContext();
//--从JWT的工具类中获取
Map<String, Object> userinfo = JWTUtil.getTokenInfo(token, "userinfo");
//获取用户名
Object username = userinfo.get("username");
//获取权限码-->并转成需要的集合格式类型
List<String> permissions = (List<String>) userinfo.get("permissions");
List<SimpleGrantedAuthority> collect = permissions.stream()
.map(item -> new SimpleGrantedAuthority(item))
.collect(Collectors.toList());
//创建token
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, collect);
//将token放入上下文
context.setAuthentication(authenticationToken);
//④验证token成功--放行
filterChain.doFilter(request,response);
}
}
2.修改配置类