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

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值