SpringSecurity

本教程来自:尚硅谷SpringSecurity框架教程(spring security源码剖析从入门到精通)
教程分为五大部分:框架概述、入门和基本原理、基于Web的权限方案、基于微服务的权限方案、源码剖析,详细讲解了Spring Security框架,内容由浅入深,理论实践相结合,更深入源码级学习。
前置知识:javaweb,spring,springboot.

1 框架概述

1.1 概述

SpringSecurity_第1张图片

1.2 历史

SpringSecurity_第2张图片

1.3 竞品比较

1.3.1 SpringSecurity

SpringSecurity_第3张图片

1.3.2 shiro

apache的shiro:
SpringSecurity_第4张图片

1.3.3 总结

SpringSecurity_第5张图片

2 入门和基本原理

2.1 快速入门

使用springboot初始向导,在模块里选择web,security,lombok,创建好后parent改个版本,这里用2.2.1.RELEASE。
SpringSecurity_第6张图片
弄一个controller试试:
SpringSecurity_第7张图片
登录http://localhost:8080/hello,没有像预想的那样,跳hello,security,而是一个网址是http://localhost:8080/login的登录页面:
SpringSecurity_第8张图片
这说明spring security起效了,默认的带的用户是user,密码看后台输出。
SpringSecurity_第9张图片
输入用户密码后正常跳转了:
SpringSecurity_第10张图片

2.2 基本流程

参考:https://blog.csdn.net/u012702547/article/details/89629415
本质上是个过滤器链,很明显,过滤器链就是一种责任链模式,而且在过滤器组件注入容器的过程中肯定也用了代理模式。
流程:

  1. 客户端发起一个请求,进入 Security 过滤器链。
  2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
  3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
  4. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。

SpringSecurity_第11张图片
对这条过滤器链的各个进行说明:

  • WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
  • SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
  • HeaderWriterFilter:用于将头信息加入响应中。
  • CsrfFilter:用于处理跨站请求伪造。
  • LogoutFilter:用于处理退出登录。
  • UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
  • DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
  • BasicAuthenticationFilter:检测和处理 http basic 认证。
  • RequestCacheAwareFilter:用来处理请求的缓存。
  • SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
  • AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名Authentication。
  • SessionManagementFilter:管理 session 的过滤器
  • ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
  • FilterSecurityInterceptor:可以看做过滤器链的出口。
  • RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

3 基于Web的权限方案

3.1 认证

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑,只需要实现 UserDetailsService 接口即可。
在认证环节,还要用到一个别的接口,就是PasswordEncoder接口,该接口的实现类可以对密码进行加密、匹配判断等操作。

3.1.1 接口介绍:PasswordEncoder,UserDetailsService

(1) PasswordEncoder:

接口中方法介绍:
SpringSecurity_第12张图片
实现类:
SpringSecurity_第13张图片
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10。
测试该实现类:

@Test
    public void test01() {
        // 创建密码解析器
        BCryptPasswordEncoder bCryptPasswordEncoder = new
                BCryptPasswordEncoder();
        // 对密码进行加密
        String atguigu = bCryptPasswordEncoder.encode("atguigu");
        // 打印加密之后的数据
        System.out.println("加密之后数据:\t" + atguigu);
        //判断原字符加密后和加密之前是否匹配
        boolean result = bCryptPasswordEncoder.matches("atguigu", atguigu);
        // 打印比较结果
        System.out.println("比较结果:\t" + result);
    }

在这里插入图片描述
这里给个可复制的数据,这是atguigu的密文,可以模拟从数据库里查出来的password的结果:

加密之后数据:	$2a$10$JDIsewXw0fYIr/L7SKho4uWlnY8Enhk5c2zdJ2WdYz4H.V6t2b22e
比较结果:	true

(2) UserDetailsService :

在这里插入图片描述
返回值UserDetails,也是个接口,是系统默认的用户主体:
SpringSecurity_第14张图片
SpringSecurity_第15张图片
实现这个UserDetail接口的有一个类User,用这个类就行:
在这里插入图片描述
User类有两个有参构造方法:
SpringSecurity_第16张图片

3.1.2 实现认证小demo

  1. 首先写一个实现UserDetailsService接口的认证实现类:
package com.atguigu.securitydemo1.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.List;

@Service("userDetailService")
public class MyUserDetailService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        //public User(String username, String password, Collection authorities)
        // username:用户名,应该是传入的s
        // password:从数据库读出来的加密的密码,这里模拟从数据库取出来,是atguigu的加密结果
        // authorities:权限信息

        String username = s;
        String password = "$2a$10$JDIsewXw0fYIr/L7SKho4uWlnY8Enhk5c2zdJ2WdYz4H.V6t2b22e";
        List<GrantedAuthority> admin = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        User user = new User(s,password,admin);
        return user;
    }
}

  1. 然后将自定义的认证实现类和加密方式都注册到配置类中:
package com.atguigu.securitydemo1.config;

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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    //教程上这里有个@Bean,但私以为这个可不加,毕竟不是autowire注入方式使用,只在这里当个常规方法使用的话可不加,经验证不加也可以
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

输入localhost:8080/hello在登陆窗口输atguigu,atguigu,
可以进入hello页面就算成功。

3.1.3 带数据库(druid+mybatisplus)实现认证的案例

  1. 首先数据库弄好:
    建个数据库learn_security,表名users.
    建表语句:
CREATE TABLE users(
 id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(20) UNIQUE NOT NULL,
PASSWORD VARCHAR(100)
);
INSERT INTO users VALUES(1,'张san','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');

INSERT INTO users VALUES(2,'李si','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');

其中密码模拟的是atguigu加密后的。

  1. 然后mybatisplus的数据库操作接口弄好
    依赖:

        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.0.5version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druid-spring-boot-starterartifactId>
            <version>1.1.17version>
        dependency>

配yaml:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/learn_security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver

    druid:
      aop-patterns: com.atguigu.securitydemo1.*  #监控SpringBean
      filters: stat,wall     # 底层开启功能,stat(sql监控),wall(防火墙)
# 配置mybatis规则
mybatis:
  #全局配置文件,autoconfig都配好了,可以不配
  #config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true #开启数据库表里面的字段名xxx_yyy和实体类里面的属性名xxxYyy的自动驼峰对应。

建users表的实体类:

import lombok.Data;

@Data
public class Users {

    private Integer id;
    private String username;
    private String password;
}

建UsersMapper接口,实现basemapper,自带基本的crud:

package com.atguigu.securitydemo1.mapper;

import com.atguigu.securitydemo1.entity.Users;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

//要么这里加@Mapper,要么启动类上加@MapperScan("com.atguigu.securitydemo1.mapper")
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}

  1. 然后实现UserDetailsService接口的认证类弄好
package com.atguigu.securitydemo1.service;

import com.atguigu.securitydemo1.entity.Users;
import com.atguigu.securitydemo1.mapper.UsersMapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.List;

@Service("userDetailService")
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UsersMapper usersMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        //根据用户名查询
        QueryWrapper<Users> queryWrapper = new QueryWrapper();
        queryWrapper.eq("username", s);
        Users users = usersMapper.selectOne(queryWrapper);

        if (users == null) {
            throw new UsernameNotFoundException("username is not exist");
        }
        String username = s;
        List<GrantedAuthority> admin = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        User user = new User(s, users.getPassword(), admin);
        return user;
    }
}

实现WebSecurityConfigurerAdapter的SecurityConfig不用变,维持3.1.2节代码。
4. 登录测试
网址:http://localhost:8080/hello,弹出login页,输入正确的会跳到hello页就算成功,账号密码对不上会显示Bad credentials,不跳转:
SpringSecurity_第17张图片

3.1.4 自定义用户登录页面

上一节虽然实现了账号密码从数据库比对,但登录页是springsecurity自带的,我们要替换成自己的。
目录结构如下:
SpringSecurity_第18张图片
MySecurityConfig:

package com.atguigu.securityatguigu.config;

import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义登陆页面
        http.formLogin()
                .loginPage("/login.html")   //登录页
                .loginProcessingUrl("/user/login")  //登录数据访问的controller路径,这个路径不用配controller,是security自己在管理
                .defaultSuccessUrl("/test/index").permitAll()  //登陆成功后跳转的路径
             .and().authorizeRequests()
                .antMatchers("/","/test/hello","/user/login").permitAll()  //设置不需要认证即可访问的路径
                .anyRequest().authenticated()
             .and().csrf().disable();   //关闭csrf防护
    }

    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

TestController:

package com.atguigu.securityatguigu.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/hello")
    public String hello() {
        return "hello,security";
    }

    @GetMapping("/index")
    public String index() {
        return "hello,index";
    }
}

login.html:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<h2>mylogin-pageh2>
<form action="/user/login" method="post">
    username:<input type="text" name="username"><br>
    password:<input type="password" name="password"><br>
    <input type="submit" value="登录">
form>
body>
html>

其他的参考上节案例。

3.2 授权

3.2.0 RBAC

每个用户的访问权限我们是通过给不同的用户赋予不同角色,角色不同,权限不同。
SpringSecurity_第19张图片
这就是为什么我们要给每个user一个角色,而不是单独规定user的访问权限。
一般我们会设计5个表:

  • 用户表
    用户名,密码,是否启用,是否锁定。。。。
  • 角色表
    角色名称,角色描述
  • 用户-角色关联表
    多对多关系
  • 权限表
    权限名,权限规定(比如可以访问的url等)
  • 权限-角色关联表
    多对多关系

3.2.1 基于权限的访问控制

hasAuthority(String authority)

在MySecurityConfig中添加:
SpringSecurity_第20张图片
数据库users表多加一列role,实体类也跟着改,MyUserDetailService也改:
在这里插入图片描述
SpringSecurity_第21张图片
SpringSecurity_第22张图片
测试使用不同role的账号登陆,当使用李si账号访问index页会报403错误,就是权限不足:
SpringSecurity_第23张图片

hasAnyAuthority(String… authorities)

hasAuthority(String authority)只能控制一种角色,当一个url多种角色都可以访问时,可以用hasAnyAuthority(String… authorities):
SpringSecurity_第24张图片

3.2.2 基于角色的访问控制

hasRole(String role)

hasAnyRole(String… roles)

使用和3.2.1节的两个方法一致,但有略微不同:
SpringSecurity_第25张图片
SpringSecurity_第26张图片

3.2.3 自定义403页面

SpringSecurity_第27张图片
SpringSecurity_第28张图片

3.3 web权限方案注解版

角色和权限控制的注解形式:
角色:
@Secured({“ROLE_admin”,“ROLE_normal”})
@PreAuthorize(“hasAnyRole(“ROLE_admin”,“ROLE_normal”)”)
权限:
@PreAuthorize(“hasAnyAuthority(“admin”,“normal”)”)
@PostAuthorize(“hasAnyAuthority(“admin”,“normal”)”) :使用并不多,在方法执行后再进行权限验证,适合验证带有返回值
的权限。
在这里插入图片描述
使用基于权限认证进行测试时,不要忘了改MyUserDetailService,去掉ROLE_前缀:
在这里插入图片描述
过滤:
SpringSecurity_第29张图片
SpringSecurity_第30张图片
建个index.html做测试:
SpringSecurity_第31张图片
修改MySecurityConfig:
登陆跳转设为index.html(不知道为什么这个在登陆后不起效。。。),
不需要认证的路径去掉“/”,
增加注解@EnableGlobalMethodSecurity(securedEnabled = true)

package com.atguigu.securityatguigu.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义登陆页面
        http.formLogin()
                .loginPage("/login.html")   //登录页
                .loginProcessingUrl("/user/login")  //登录数据访问的controller路径,这个路径不用配controller,是security自己在管理
                .defaultSuccessUrl("/index.html").permitAll()  //登陆成功后跳转的路径
                .and().authorizeRequests()
                .antMatchers( "/test/hello", "/user/login").permitAll()  //设置不需要认证即可访问的路径
                .anyRequest().authenticated()
                .and().csrf().disable();   //关闭csrf防护

        http.exceptionHandling().accessDeniedPage("/unauth.html");
    }
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

TestController在原来基础上加两个路径:

	@GetMapping("/admin")
    @Secured({"ROLE_admin"})
    public String admin(){
        return "admin-page";
    }
    @GetMapping("/normal")
    @Secured({"ROLE_admin","ROLE_normal"})
    public String normal(){
        return "normal-page";
    }

测试登录role是normal的李si用户,发现点击admin的链接是自定义的403,normal可以正常进入。

3.4 补充:注销

SpringSecurity_第32张图片
SpringSecurity_第33张图片

3.5 session-cookie认证与token认证介绍

之前在web我们见过一种自动登录的实现形式—基于cookie,这里介绍通过另一种方式(JWT(json web token))实现自动登录。
参考:
Token登录认证详解
JWT的原理和使用
JWT(JSON Web Token)简介

(1)session-cookie:

SpringSecurity_第34张图片
SpringSecurity_第35张图片
SpringSecurity_第36张图片
SpringSecurity_第37张图片
没有cookie,session怎么运行:
SpringSecurity_第38张图片

(2)token-JWT(json web token):

SpringSecurity_第39张图片
SpringSecurity_第40张图片
token类型:
SpringSecurity_第41张图片
JWT组成:
SpringSecurity_第42张图片
SpringSecurity_第43张图片
SpringSecurity_第44张图片
SpringSecurity_第45张图片
SpringSecurity_第46张图片

3.6 csrf:跨站请求攻击

什么是CSRF?可能多数人都不清楚,没事,一起来了解!!!

4 基于微服务的权限方案

认证里面很重要的一点就是单点登录,就是说一个用户在一个微服务模块登录后访问其他模块不需要登陆。
认证授权过程:
SpringSecurity_第47张图片
权限管理模型(RBAC方式):
SpringSecurity_第48张图片

4.1 案例实现

教学代码下载:https://download.csdn.net/download/anotherQu/49600330
sql代码可以取自:一个atguigu学生的gitee-springsecurity代码笔记

主要实现三点:

  1. 登录认证
  2. 添加用户
  3. 添加角色
  4. 为用户分配角色
  5. 为角色分配权限

使用技术:

  • 后端:
    maven:项目构建,父工程管理版本依赖,子模块使用依赖。
    springboot:微服务的基本子模块
    mybatisplus:数据库操作框架
    springcloud技术: gateway网关、nacos注册中心
    redis: nosql数据库
    jwt:json web token,基于json、token的权限认证规范
    swagger: 用来测试的东西
  • 前端:
    vue,vue-cli,element-ui

4.1.1 搭建项目工程

(1)模块搭建

教学代码下载:https://download.csdn.net/download/anotherQu/49600330
sql代码可以取自:一个atguigu学生的gitee-springsecurity代码笔记
SpringSecurity_第49张图片

(2)redis,nacos启动

redis和nacos的安装和启动请参考尚硅谷相关的视频教程。

  1. 启动redis,这里用windows版做演示,实际环境肯定是lunix版。
    SpringSecurity_第50张图片
  2. 启动nacos注册中心
    去官网上下个nacos1.1.4的源码:https://github.com/alibaba/nacos/tree/1.1.4
    SpringSecurity_第51张图片
    在解压目录下mvn处理源码:
    命令:mvn -Prelease-nacos -DskipTests clean install -U
    在这里插入图片描述
    处理后得到nacos-server:
    SpringSecurity_第52张图片
    解压nacos-server后,点击bin下的 start.cmd运行nacos。
    访问 http://localhost:8848/nacos/#/login 账号密码:nacos/nacos
    SpringSecurity_第53张图片

(3)spring_security模块

其他模块不做说明,这里只展示security模块的相关代码
SpringSecurity_第54张图片
TokenWebSecurityConfig:

package com.atguigu.security.config;

import com.atguigu.security.filter.TokenAuthFilter;
import com.atguigu.security.filter.TokenLoginFilter;
import com.atguigu.security.security.DefaultPasswordEncoder;
import com.atguigu.security.security.TokenLogoutHandler;
import com.atguigu.security.security.TokenManager;
import com.atguigu.security.security.UnauthEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
//security总配置类
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userDetailsService;
    private DefaultPasswordEncoder defaultPasswordEncoder;
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    @Autowired
    public TokenWebSecurityConfig(UserDetailsService userDetailsService,
                                  DefaultPasswordEncoder defaultPasswordEncoder,
                                  TokenManager tokenManager,
                                  RedisTemplate redisTemplate) {
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }


    //设置退出的地址和token,redis操作地址
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                //没有权限访问时调用自定义的处理类
                .authenticationEntryPoint(new UnauthEntryPoint())
                .and().csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()
                //退出路径
                .and()
                .logout().logoutUrl("/admin/acl/index/logout")
                // 调用退出时的处理器
                .addLogoutHandler(new TokenLogoutHandler(tokenManager, redisTemplate))
                .and()
                // 认证过滤器
                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
                // 授权过滤器
                .addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate))
                .httpBasic();
    }

    @Override
    //调用userDetailsService和密码处理
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }

    @Override
    //不进行认证的路径,可以直接访问
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/api/**");
    }

}

CurrentUserInfo:

package com.atguigu.security.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

@Data
@ApiModel(description = "用户实体类")
public class CurrentUserInfo implements Serializable {
    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "微信openid")
    private String username;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "昵称")
    private String nickName;

    @ApiModelProperty(value = "用户头像")
    private String salt;

    @ApiModelProperty(value = "用户签名")
    private String token;
}

SecurityUser:

package com.atguigu.security.entity;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Data
public class SecurityUser implements UserDetails {
    //当前登录用户
    private transient CurrentUserInfo currentUserInfo;

    //当前权限
    private List<String> permissionValueList;

    public SecurityUser() {
    }

    public SecurityUser(CurrentUserInfo user) {
        if (user != null) {
            this.currentUserInfo = user;
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (String permissionValue : permissionValueList) {
            if (StringUtils.isEmpty(permissionValue)) {
                continue;
            }
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
            authorities.add(authority);
        }

        return authorities;
    }

    @Override
    public String getPassword() {
        return currentUserInfo.getPassword();
    }

    @Override
    public String getUsername() {
        return currentUserInfo.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

TokenAuthFilter:

package com.atguigu.security.filter;

import com.atguigu.security.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

//授权过滤器
@SuppressWarnings("unchecked")
public class TokenAuthFilter extends BasicAuthenticationFilter {
    private TokenManager tokenManager;

    private RedisTemplate redisTemplate;

    public TokenAuthFilter(AuthenticationManager authenticationManager,
                           TokenManager tokenManager,
                           RedisTemplate redisTemplate) {
        super(authenticationManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        // 获取当前认证成功的用户权限信息
        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);

        // 有权限信息 放入权限上下文中
        if (authRequest != null) {
            SecurityContextHolder.getContext().setAuthentication(authRequest);
        }
        chain.doFilter(request, response);
    }

    //从token获取用户名,从redis获取对应权限列表
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //从header获取token
        String token = request.getHeader("token");
        if (token == null) {
            return null;
        }
        //从token获取用户名
        String username = tokenManager.getUserInfoFromToken(token);

        //从redis获取对应权限列表
        List<String> list = (List<String>) redisTemplate.opsForValue().get(username);
        if (null == list) {
            return null;
        }

        Collection<GrantedAuthority> authorities = new ArrayList<>();
        list.forEach(item -> {
            authorities.add(new SimpleGrantedAuthority(item));
        });
        return new UsernamePasswordAuthenticationToken(username, token, authorities);

    }
}

TokenLoginFilter

package com.atguigu.security.filter;

import com.atguigu.security.entity.CurrentUserInfo;
import com.atguigu.security.entity.SecurityUser;
import com.atguigu.security.security.TokenManager;
import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

//认证过滤器
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    /**
     * 权限
     */
    private AuthenticationManager authenticationManager;
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenLoginFilter(AuthenticationManager authenticationManager,
                            TokenManager tokenManager,
                            RedisTemplate redisTemplate) {
        this.authenticationManager = authenticationManager;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.setPostOnly(false);
        // 设置登陆路径,并且post请求
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login", "POST"));
    }

    @Override
    //获取表单提交的相关信息
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        // 获取表单提交数据
        try {
            CurrentUserInfo user = new ObjectMapper().readValue(request.getInputStream(), CurrentUserInfo.class);

            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            user.getUsername(), user.getPassword(), new ArrayList<>()));

        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }

    }

    @Override
    //认证成功调用的方法 生成token 存入到redis
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        // 认证成功之后,得到认证成功后的用户信息
        SecurityUser user = (SecurityUser) authResult.getPrincipal();

        // 根据用户名生成token
        String token = tokenManager.createToken(user.getUsername());

        // 把用户名和用户权限放入redis中
        redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());

        ResponseUtil.out(response, R.ok().data("token", token));
    }


    @Override
    //认证失败调用的方法
    protected void unsuccessfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response,
                                              AuthenticationException failed) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}

DefaultPasswordEncoder:

package com.atguigu.security.security;

import com.atguigu.utils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
//密码处理工具类
public class DefaultPasswordEncoder implements PasswordEncoder {

    public DefaultPasswordEncoder() {
        this(-1);
    }

    public DefaultPasswordEncoder(int strength) {

    }

    //MD5加密
    @Override
    public String encode(CharSequence charSequence) {
        return MD5.encrypt(charSequence.toString());
    }

    //密码比对
    @Override
    public boolean matches(CharSequence charSequence, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
    }

    @Override
    public boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

TokenLogoutHandler:

package com.atguigu.security.security;

import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

//退出处理器 退出时移除token 并删除redis中的token信息
public class TokenLogoutHandler implements LogoutHandler {

    private TokenManager tokenManager;

    private RedisTemplate redisTemplate;

    public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
        //1 从header里面获取token
        //2 token不为空,移除token,从redis删除token
        String token = httpServletRequest.getHeader("token");
        if (token != null) {
            //移除
            tokenManager.removeToken(token);
            //从token获取用户名
            String username = tokenManager.getUserInfoFromToken(token);
            redisTemplate.delete(username);
        }
        ResponseUtil.out(httpServletResponse, R.ok());
    }
}

TokenManager:

package com.atguigu.security.security;

import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class TokenManager {

    //token有效时常 毫秒单位
    private long tokenEcpiration = 24 * 60 * 60 * 1000;

    //编码密钥,应该是自动生成,这里为了简单写死
    private String tokenSignKey = "123456";

    //根据用户名生成token
    public String createToken(String username) {
        String token = Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + tokenEcpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
        return token;
    }

    //根据token字符串得到用户信息
    public String getUserInfoFromToken(String token) {
        String userinfo = Jwts.parser()
                .setSigningKey(tokenSignKey)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
        return userinfo;
    }

    //删除token
    public void removeToken(String token) {
    }
}

UnauthEntryPoint:

package com.atguigu.security.security;

import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class UnauthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(httpServletResponse, R.error());
    }
}

(4)前端项目

根路径下:

npm install
npm run dev

SpringSecurity_第55张图片

(5)测试

前端启动:npm run dev
后端启动:nacos,redis,gateway模块,service_acl模块
访问:

http://localhost:9528

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