spring secuity(三更草堂)

一、介绍

spring security是spring家族的一个安全管理框架,适用于大中型项目。
shiro也是一个安全管理框架,但他适用于小型项目。

安全框架主要做两件事:
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:判断登录后的用户是否有权限进行某个操作

二、快速入门

首先添加依赖,启动项目即可:

        <!--安全框架springsecurity依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

启动后,控制台会有登录密码,用户名为user

访问:
spring secuity(三更草堂)_第1张图片

四、jwt(b站-编程不良人)

1.什么是jwt?

官网地址: https://jwt.io/introduction/

JWT是Json Web Token,也就是通过JSON的形式作为web应用中的令牌,用于在各方之间安全的将信息作为JSON对象传输,在传输过程中还可以完成数据加密、签名等相关处理。

2.JWT能做什么?

1.)授权

一旦用户登录以后,每个后续请求将包含JWT。单点登录是当今广泛使用jwt’的一项功能,因为他的开销很小,并且可以在不同的域中轻松使用。

2.)信息交换

在各方之间安全的传输信息,通过对jwt进行签名(使用公钥/私钥对),所以可以确保发件人是他们所说的人,此外由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改.

3.)session的缺点

我们知道,http协议是无状态的协议(不会保持用户的登录状态),为了防止每一次请求跳转的时候都要重新登录,前期我们是通过session来保持用户的登录状态。

但是通过session保持用户的登录状态有以下几个缺点:
①session是保存在服务器端的内存当中,随着登录用户的不断增多,服务器端需要的内存会比较大,造成成本增加。

②如果是分布式项目还要涉及到分布式session方案的设计。

③由于session是通过cookie传输sessionid来进行工作的,如果cookie被截取,用户很容易遭受到跨站请求伪造的攻击。

④在前后端分离的情况下,前端的请求会经过很多的中间件,每次请求转发都会到服务器验证,造成服务器压力增大
spring secuity(三更草堂)_第2张图片

4.)基于jwt的认证流程和jwt认证的优势

spring secuity(三更草堂)_第3张图片

认证流程:

①前端通过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形式都支持。

④不需要在服务端保存会话信息,特别适用于分布式微服务。

5.)jwt的结构

jwt是由标头、有效载荷、签名组成。

①标头

  • 标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。

  • 注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

举例:

{
  "alg": "HS256",
  "typ": "JWT"
}

②有效载荷(不要放用户的敏感信息)

  • 令牌的第二部分是有效负载,其中包含声明。声明是有关实体
  • (通常是用户)和其他数据的声明。同样的,它会使用
    Base64 编码组成 JWT 结构的第二部分
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

③签名

  • 前面两部分都是使用 Base64 进行编码的,即前端可以解开知
    道里面的信息。Signature 需要使用编码后的 header 和 payload
    以及我们提供的一个密钥,然后使用 header 中指定的签名算法
    (HS256)进行签名。签名的作用是保证 JWT 没有被篡改过

  • 最后一步签名的过程,实际上是对头部以及负载内容进行签名,
    防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修
    改,再进行编码,最后加上之前的签名组合形成新的JWT的话,
    那么服务器端会判断出新的头部和负载形成的签名和JWT附带上
    的签名是不一样的。如果要对新的头部和负载进行签名,在不知
    道服务器加密时用的密钥的话,得出来的签名也是不一样的。

- 在这里大家一定会问一个问题:Base64是一种编码,
是可逆的,那么我的信息不就被暴露了吗?

  • 是的。所以,在JWT中,不应该在负载里面加入任何敏感的
    数据。在上面的例子中,我们传输的是用户的User ID。这个值
    实际上不是什么敏 感内容,一般情况下被知道也是安全的。但
    是像密码这样的内容就不能被放在JWT中了。如果将用户的密
    码放在了JWT中,那么怀有恶意的第 三方通过Base64解码就能
    很快地知道你的密码了。因此JWT适合用于向Web应用传递一些
    非敏感信息。JWT还经常用于设计用户认证和授权系 统,甚至
    实现Web应用的单点登录。

以上显示的是未编码的信息,编码后的jwt是:xxxxx.yyyyy.zzzzz

6.)JWT的简单使用

引入依赖:

<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异常

7.)工具类

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接口,重写其中的方法。
spring secuity(三更草堂)_第4张图片
因为要返回UserDetails类型的对象,所以我们实现一个:
spring secuity(三更草堂)_第5张图片

六、自定义登录接口

spring security会把所有的请求都拦截,实际情况下,用户是可以进入登录页面的,所以我们需要把登录页面放行,让用户在登录页面进行登录操作

同时,在以前的入门案例中,在springsecurity官方定义的登录界面进行用户认证,我们重写了官方定义的登录界面就要重写用户认证方面的问题:可以通过通过AuthenticationManager的authenticate方法来进行用户认证

注入AuthenticationManager
spring secuity(三更草堂)_第6张图片
在业务逻辑中重新定义认证
spring secuity(三更草堂)_第7张图片
spring secuity(三更草堂)_第8张图片

七、利用过滤器拦截请求,验证token

在前面的自定义登录接口中,登录后会返回一个token,以后每次请求时请求头都会携带这个token,所以我们需要定义一个拦截器,拦截这些请求,验证token,查看是否已登录

①自定义过滤器:
spring secuity(三更草堂)_第9张图片
②配置自定义过滤器,并将其置于授权过滤器之前
spring secuity(三更草堂)_第10张图片
③结果
spring secuity(三更草堂)_第11张图片
spring secuity(三更草堂)_第12张图片

八、退出登录

如果用户登录会在redis当中存入当前用户的信息
注销登录的方法就是:删除redis当中的用户的信息

逻辑代码:
在这里插入图片描述
结果:
spring secuity(三更草堂)_第13张图片
spring secuity(三更草堂)_第14张图片
spring secuity(三更草堂)_第15张图片
spring secuity(三更草堂)_第16张图片

九、权限系统(不同的用户拥有不同的权限)

实现授权的基本思路:
①SpringSecurity使用FilterSecurityInterceptor进行权限校验,FilterSecurityInterceptor会从SecurityContextHolder中获取Authentication,从中获取用户的权限信息,所以我们要把用户的权限信息也存入Authentication。
②在资源(接口)上通过框架或自定义注解设置权限

-----------------------------------实操------------------------------------------------------

1.)限制访问资源所需的权限

spring secuity(三更草堂)_第17张图片
spring secuity(三更草堂)_第18张图片

2.)用户登录时将权限信息保存到SecurityContextHolder

spring secuity(三更草堂)_第19张图片

spring secuity(三更草堂)_第20张图片
spring secuity(三更草堂)_第21张图片

十、从数据库查询权限信息

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

spring secuity(三更草堂)_第22张图片

建表语句:


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;

逻辑处理没什么难的,就是将以前写死的地方,用数据库查询代替:
spring secuity(三更草堂)_第23张图片
spring secuity(三更草堂)_第24张图片

十一、自定义异常处理

当认证失败或授权失败时,应该返回特定的异常信息给前端。
这里以前也学过springboot的全局异常处理,也可以用全局异常处理来解决,但是这里SpringSecurity有自己的异常处理机制,最好用SpringSecurity


主要思想:
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

​ 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

​ 所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

在这里插入图片描述
spring secuity(三更草堂)_第25张图片
最后配置给SpringSecurity的配置类:
spring secuity(三更草堂)_第26张图片
结果:
spring secuity(三更草堂)_第27张图片

十二、跨域问题解决

前后端分离项目中,前端和后端部署的服务器一般是不同的,即前端项目和后端项目的 域名、端口、协议 可能不同。在用浏览器访问前端项目向后端项目发送请求时,会因为前端项目和后端项目的 域名、端口、协议 可能不同,导致请求失败。(像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();
    }
    
}

十三、其他权限校验方法

spring secuity(三更草堂)_第28张图片
以上的权限校验方法是SpringSecurity官方定义的,在实际开发中,权限校验方法比较复杂需要自定义权限校验方法:
①设计权限验证逻辑spring secuity(三更草堂)_第29张图片

②在接口上配置对应的权限验证方法
spring secuity(三更草堂)_第30张图片

也可以使用配置的方法为接口设置权限
spring secuity(三更草堂)_第31张图片

十四、CSRF

CSRF(Cross-site request forgery)跨站请求伪造,是web常见的攻击之一。
spring secuity(三更草堂)_第32张图片
如果不是前后端分离的话,并且还使用了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的课件。

你可能感兴趣的:(spring,java,http)