spring security是spring家族的一个安全管理框架,适用于大中型项目。
shiro也是一个安全管理框架,但他适用于小型项目。
安全框架主要做两件事:
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:判断登录后的用户是否有权限进行某个操作
首先添加依赖,启动项目即可:
<!--安全框架springsecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
启动后,控制台会有登录密码,用户名为user
官网地址: https://jwt.io/introduction/
JWT是Json Web Token,也就是通过JSON的形式作为web应用中的令牌,用于在各方之间安全的将信息作为JSON对象传输,在传输过程中还可以完成数据加密、签名等相关处理。
一旦用户登录以后,每个后续请求将包含JWT。单点登录是当今广泛使用jwt’的一项功能,因为他的开销很小,并且可以在不同的域中轻松使用。
在各方之间安全的传输信息,通过对jwt进行签名(使用公钥/私钥对),所以可以确保发件人是他们所说的人,此外由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改.
我们知道,http协议是无状态的协议(不会保持用户的登录状态),为了防止每一次请求跳转的时候都要重新登录,前期我们是通过session来保持用户的登录状态。
但是通过session保持用户的登录状态有以下几个缺点:
①session是保存在服务器端的内存当中,随着登录用户的不断增多,服务器端需要的内存会比较大,造成成本增加。
②如果是分布式项目还要涉及到分布式session方案的设计。
③由于session是通过cookie传输sessionid来进行工作的,如果cookie被截取,用户很容易遭受到跨站请求伪造的攻击。
④在前后端分离的情况下,前端的请求会经过很多的中间件,每次请求转发都会到服务器验证,造成服务器压力增大
认证流程:
①前端通过Web表单将自己的用户名和密码发送到后端的接口。
这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加
密的传输(https协议),从而避免敏感信息被嗅探。
②后端核对用户名和密码成功后,将用户的id等其他信息作为
JWT Payload(负载),将其与头部分别进行Base64编码拼接后
签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx
的字符串。 token head.payload.singurater
③
后端将JWT字符串作为登录成功的返回结果返回给前端。前端可
以将返回的结果保存在localStorage或sessionStorage上,退出登录
时前端删除保存的JWT即可。
④前端在每次请求时将JWT放入HTTP Header中的Authorization位。
(解决XSS和XSRF问题) HEADER
⑤后端检查是否存在,如存在验证JWT的有效性。例如,检查签名
是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
⑥验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,
返回相应结果。
优势:
①简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,
因为数据量小,传输速度也很快
②自包含(Self-contained):负载中包含了所有用户所需要的信息,
避免了多次查询数据库
③因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语
言的,原则上任何web形式都支持。
④不需要在服务端保存会话信息,特别适用于分布式微服务。
jwt是由标头、有效载荷、签名组成。
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。
注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
举例:
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
前面两部分都是使用 Base64 进行编码的,即前端可以解开知
道里面的信息。Signature 需要使用编码后的 header 和 payload
以及我们提供的一个密钥,然后使用 header 中指定的签名算法
(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
最后一步签名的过程,实际上是对头部以及负载内容进行签名,
防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修
改,再进行编码,最后加上之前的签名组合形成新的JWT的话,
那么服务器端会判断出新的头部和负载形成的签名和JWT附带上
的签名是不一样的。如果要对新的头部和负载进行签名,在不知
道服务器加密时用的密钥的话,得出来的签名也是不一样的。
- 在这里大家一定会问一个问题:Base64是一种编码,
是可逆的,那么我的信息不就被暴露了吗?
以上显示的是未编码的信息,编码后的jwt是:xxxxx.yyyyy.zzzzz
引入依赖:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
public class JwtTest {
/**
* 生成token
*/
@Test
public void createTest(){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 90);
String token = JWT.create()
.withClaim("username", "mcf")
.withClaim("admin", true)
// 设置过期时间
.withExpiresAt(instance.getTime())
// 设置签名
.sign(Algorithm.HMAC256("token!Q2W#E$RW"));
System.out.println(token);
}
/**
* 解析token,输出相关信息
*/
@Test
public void paraseInfo(){
JWTVerifier build = JWT.require(Algorithm.HMAC256("token!Q2W#E$RW")).build();
DecodedJWT verify = build.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNjUxMTk4Mzg3LCJ1c2VybmFtZSI6Im1jZiJ9.tH5-qe60SUmBC_7cO-QJv-86PURd3QkiGYVhBafZs-I");
System.out.println(verify.getClaim("username").asString());
System.out.println(verify.getClaim("admin").asBoolean());
}
}
可能出现的常见日常:
SignatureVerificationException: 签名不一致异常
TokenExpiredException: 令牌过期异常
AlgorithmMismatchException: 算法不匹配异常
InvalidClaimException: 失效的payload异常
public class JWTUtils {
private static String TOKEN = "token!Q@W3e4r";
/**
* 生成token
* @param map //传入payload
* @return 返回token
*/
public static String getToken(Map<String,String> map){
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,7);
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(TOKEN));
}
/**
* 验证token
* @param token
* @return
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token); // 如果验证通过,则不会把报错,否则会报错
}
/**
从DecodedJWT 中拿到所有的信息
* 获取token中payload
* @param token
* @return
*/
public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
}
数据库信息自己补充,或到百度网盘中找。
创建一个类实现UserDetailsService接口,重写其中的方法。
因为要返回UserDetails类型的对象,所以我们实现一个:
spring security会把所有的请求都拦截,实际情况下,用户是可以进入登录页面的,所以我们需要把登录页面放行,让用户在登录页面进行登录操作。
同时,在以前的入门案例中,在springsecurity官方定义的登录界面进行用户认证,我们重写了官方定义的登录界面就要重写用户认证方面的问题:可以通过通过AuthenticationManager的authenticate方法来进行用户认证
注入AuthenticationManager
在业务逻辑中重新定义认证
在前面的自定义登录接口中,登录后会返回一个token,以后每次请求时请求头都会携带这个token,所以我们需要定义一个拦截器,拦截这些请求,验证token,查看是否已登录
①自定义过滤器:
②配置自定义过滤器,并将其置于授权过滤器之前
③结果
如果用户登录会在redis当中存入当前用户的信息
注销登录的方法就是:删除redis当中的用户的信息
实现授权的基本思路:
①SpringSecurity使用FilterSecurityInterceptor进行权限校验,FilterSecurityInterceptor会从SecurityContextHolder中获取Authentication,从中获取用户的权限信息,所以我们要把用户的权限信息也存入Authentication。
②在资源(接口)上通过框架或自定义注解设置权限
-----------------------------------实操------------------------------------------------------
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
建表语句:
CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `sg_security`;
/*Table structure for table `sys_menu` */
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
`path` varchar(200) DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
/*Table structure for table `sys_role` */
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(200) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
/*Table structure for table `sys_role_menu` */
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
/*Table structure for table `sys_user` */
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` varchar(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) DEFAULT NULL COMMENT '头像',
`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
/*Table structure for table `sys_user_role` */
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
逻辑处理没什么难的,就是将以前写死的地方,用数据库查询代替:
当认证失败或授权失败时,应该返回特定的异常信息给前端。
(这里以前也学过springboot的全局异常处理,也可以用全局异常处理来解决,但是这里SpringSecurity有自己的异常处理机制,最好用SpringSecurity)
主要思想:
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
前后端分离项目中,前端和后端部署的服务器一般是不同的,即前端项目和后端项目的 域名、端口、协议 可能不同。在用浏览器访问前端项目向后端项目发送请求时,会因为前端项目和后端项目的 域名、端口、协议 可能不同,导致请求失败。(像apipost等测试工具不会出现此类现象)
springboot开启跨域请求:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
SpringSecurity开启跨域请求:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//ctrl+o可以查看一个类中所有的可被重写的方法
//将AuthenticationManager注入到spring容器中
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置某个接口可以匿名访问......
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
//关闭csrf 详细介绍:https://blog.csdn.net/weixin_40482816/article/details/114301717
csrf().disable()
//不从session中获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
//除了上面的请求全部需要鉴权认证
.anyRequest().authenticated();
//添加我们的自定义过滤器,并将其置于授权过滤器UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//配置自定义异常处理器
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).
accessDeniedHandler(accessDeniedHandler);
//开启跨域请求
http.cors();
}
}
以上的权限校验方法是SpringSecurity官方定义的,在实际开发中,权限校验方法比较复杂需要自定义权限校验方法:
①设计权限验证逻辑
CSRF(Cross-site request forgery)跨站请求伪造,是web常见的攻击之一。
如果不是前后端分离的话,并且还使用了SpringSecurity框架,那么SpringSecurity会借助csrf_token来防范CSRF攻击,后端会生成一个csrf_token,每次前端请求的时候都会携带csrf_token,后端会有过滤器拦截看是否有csrf_token或者是否csrf_token是仿造的。
如果是前后端分离的情况下的话,我们一般是使用token来验证用户信息,而CSRF需要借助cookie来完成攻击,前后端分离项目没有cookie,天然的避免了CSRF攻击。但是拦截器是默认会验证csrf_token的,所以在前后端分离的项目中需要关闭csrf防范;
实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。
UsernamePasswordAuthenticationFilter是SpringSecurity官方用于用户认证的过滤器,但是我们前几节学的时候,自定义了登录认证接口,通过我们自定义的登录接口来实现认证,所以在我们自定义登录接口的项目中是没有UsernamePasswordAuthenticationFilter,使用了自定义的登录接口替换了UsernamePasswordAuthenticationFilter,也就不能使用认证成功处理器。
如果我以后的项目中用到了这些知识,可以在百度网盘中找三更草堂的SpringSecurity的课件。