SpringSecurity

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的方式更适合。

  1. 使用前后端分离或后台使用了集群--一定采用token模式。--一般线上的都是这种
  2. 传统的项目前端和后端都在一个工程下--基于session模式。--公司的系统(只需要很少的人登录)、仓库管理。。。

1.3什么是授权

还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的==权限==才可以正常使用发送红包==功能==,拥有发朋友圈功能的权限才可以便用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。

1.权限【权限表】--资源【接口】

(一期项目的权限管理--不同的角色有不同的功能)

1.3.1为什么要授权

认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,==授权是在认证通过后发生的==,控制不同的用户能够访问不同的资源。授权:授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

认证授权的框架:

[1]shiro 轻量级的认证授权 它可以整合任意框架 它支持javase和javaee

[2]springsecurity 重量级的认证授权框架。它只能和spring整合,只支持javaee web框架。

spring非常麻烦,但是现在和springboot整合就很简单了。

3.概述springsecurity

Spring Security

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.修改配置类

相关推荐
小天努力学java27 分钟前
【面试系列】深入浅出 Spring
java·spring·面试
Just_Paranoid31 分钟前
解析 Java 项目生成常量、变量和函数 Excel 文档
java·python·正则表达式·excel·javadoc
阿垂爱挖掘31 分钟前
34 - Java 8 Stream
java
simple_ssn1 小时前
【蓝桥杯】压缩字符串
java·算法
舒克日记2 小时前
Java:189 基于SSM框架的在线电影评价系统
java·开发语言
2401_857610032 小时前
中文学习系统:成本效益分析与系统优化
java·数据库·学习·架构
nbsaas-boot2 小时前
如何更高效地使用乐观锁提升系统性能
java·服务器·数据库
转转技术团队2 小时前
【述职黑话】ToB交易业务解决方案之状态机
java·状态模式
darkdragonking2 小时前
解决POM依赖与maven仓库关联的问题
java·maven
m0_672449602 小时前
前后端分离(前端删除数据库数据)
java·数据库·mysql