spring-security学习心得

1. 简介

1. 概述

Spring是非常流行和成功的Java应用开发框架,Spring Security正是Spring家族中的成员。Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。|
正如你可能知道的关于安全方面的两个主要区域是“认证和授权”(或者访问控制),一般来说,Web应用的安全性包括**用户认证(Authentication)和用户授权( Authonization)**两个部分,这两点也是Spring Security重要核心功能。

  • 用户认证指的是∶验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录
  • 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

2. 入门案例

1.创建springboot工程

1.引入依赖

springboot版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <version>2.4.0</version>
    </dependency>
</dependencies>

springcloud版本

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>

书写启动类之后,控制台会打印默认用户 user 的密码

security默认生成随机密码

2. 底层过滤器

用途
FilterSecurityInterceptor最底层的方法级过滤器
ExceptionTranslationFilter异常过滤器用来处理认证授权过程中抛出的异常
UsernamePasswordAuthenticationFilter接受POST请求过来的参数,对登录的请求进行拦截,校验表单中的用户名和密码

1. 过滤器执行流程

  1. 使用Spring Security配置过滤器DelegatingFilterProxy

  2. public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized(this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = this.findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
                    }
    
                    delegateToUse = this.initDelegate(wac);
                }
    
                this.delegate = delegateToUse;
            }
        }
    
        this.invokeDelegate(delegateToUse, request, response, filterChain);
    }
    
  3. protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        String targetBeanName = this.getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
        if (this.isTargetFilterLifecycle()) {
            delegate.init(this.getFilterConfig());
        }
    
        return delegate;
    }
    
  4. 通过FilterChainProxy中的doFilterInternal中的

    List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
    

    获取所有的过滤器加载到过滤链中

3. 两个重要的接口

  • UserDetailsService 获取数据库中的用户数据
  • PasswordEncoder 加密密码的接口

1. UserDetailsService

  1. 书写一个类继承UsernamePasswordAuthenticationFilter,重写其中的三个方法
  2. 创建类实现UserDetailService,编写查询数据过程,返回User对象(SpringSecurity提供)

2. PasswordEncoder

  1. 数据加密接口,用于返回User对象里边的密码加密

3. web权限方案

  • 主要就是成**认证和授权**

1. 通过配置文件配置

在项目中的application.yml中添加以下内容即可

spring:
  security:
    user:
      name: root
      password: root

2. 通过配置类来配置

书写的配置类**必须继承WebSecurityConfigurerAdapter** , 而且需要返回一个PasswordEncoder的对象,否则就会报错

package com.javacode.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;

/**
 * @author zhaojinhui
 * @date 2020/12/15 11:21
 * @apiNote
 */
@Configuration
public class UserConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String password = encoder.encode("admin");
        auth.inMemoryAuthentication().withUser("admin").password(password).roles("admin");
    }

    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

3. 编写自定义实现类

  • 创建配置类,设置使用呢个userDetailsService实现类
  • 编写实现类,返回User对象

配置类

package com.javacode.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;

/**
 * @author zhaojinhui
 * @date 2020/12/15 14:57
 * @apiNote
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

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

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

实现类

package com.javacode.service.impl;

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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author zhaojinhui
 * @date 2020/12/15 15:00
 * @apiNote
 */
@Service
public class UserServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        return new User("root", new BCryptPasswordEncoder().encode("admin"), auth);
    }
}

4. 连接数据库查询用户角色

1.整合mybatis-plus

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <version>2.4.0</version>
    </dependency>
    <!--数据库相关-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.1.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.3.7</version>
    </dependency>
</dependencies>

2. 书写实现类

1. 简单例子 通过用户名查询
  • 必须实现UserDetailsService接口

如果不配置密码加密器会报错的

package com.javacode.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.javacode.beans.SysUser;
import com.javacode.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author zhaojinhui
 * @date 2020/12/15 15:00
 * @apiNote
 */
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserServiceImpl implements UserDetailsService {

    private final UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
        wrapper.eq("username", "123");
        SysUser sysUser = userMapper.selectOne(wrapper);
        if(ObjectUtils.isEmpty(sysUser)){
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        return new User(sysUser.getUsername(), new BCryptPasswordEncoder().encode(sysUser.getPassword()), auth);
    }
}

5. 自定义登录页

1. 配置类新增

package com.javacode.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.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;

/**
 * @author zhaojinhui
 * @date 2020/12/15 14:57
 * @apiNote
 */
@Configuration
public class WebSecurityConfig 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") //登录访问地址
            .defaultSuccessUrl("/test/index") //登录成功后的默认跳转路径
            .permitAll()
                //设置哪些路径可以直接访问不需要认证
            .and().authorizeRequests()
                .antMatchers("/","/test/hello","/add")
                .permitAll()
            .and().csrf().disable();//关闭csrf防护    
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    

}

2. 前端书写

前端的表单必须叫usernamepassword 原因https://www.bilibili.com/video/BV15a411A7kP?p=11&t=112

6. 用户授权

1. hasAuthority方法

  • 如果当前的主体由指定的权限则返回true
  • 如果没有对应的权限的话就会报错403
package com.javacode.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.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;

/**
 * @author zhaojinhui
 * @date 2020/12/15 14:57
 * @apiNote
 */
@Configuration
public class WebSecurityConfig 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") //登录访问地址
            .defaultSuccessUrl("/test/index") //登录成功后的默认跳转路径
            .permitAll()
                //设置哪些路径可以直接访问不需要认证
            .and().authorizeRequests()
                .antMatchers("/","/test/hello")
                .permitAll()
                .antMatchers("/test/index").hasAnyAuthority("admin")
            //这两种都是可以的
            //.hasAnyAuthority("admin,user")
            //.hasAnyAuthority("admin","user")
            .and().csrf().disable()
            ;//关闭csrf防护
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


}

2. hasAnyAuthority

  • 当前主体有其中一个权限就可以访问

3. hasRole

  • 用户的角色都会被添加上ROLE_前缀 ,在org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer中的方法上
private static String hasRole(String role) {
    Assert.notNull(role, "role cannot be null");
    if (role.startsWith("ROLE_")) {
        throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
    } else {
        return "hasRole('ROLE_" + role + "')";
    }
}
  • 我们的角色设置就需要设置为
"ROLE_admin"

4. hasAnyRole

  • 有其中任意一个角色允许访问

7. 自定义403页面

config配置类中修改

package com.javacode.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.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;

/**
 * @author zhaojinhui
 * @date 2020/12/15 14:57
 * @apiNote
 */
@Configuration
public class WebSecurityConfig 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 {
    //自定义403页面
        http.exceptionHandling().accessDeniedPage("/un_auth.html");
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


}

8. 注解的使用

1. @Secured

用户具有某个角色,可以访问方法

  • 需要先启动类添加开启@EnableGlobalMethodSecurity(securedEnabled = true)
  • 在controller中的方法上添加注解,@Secured("ROLE_admin"),需要在角色信息前边添加ROLE_,否则访问了
package com.javacode;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

/**
 * @author zhaojinhui
 * @date 2020/12/8 22:56
 * @apiNote
 */
@SpringBootApplication
@MapperScan("com.javacode.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityApp {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApp.class,args);
    }
}

2. @PreAuthorize

使用前需要先开启注解,适用于进入方法前的权限验证,@PreAuthorize可以将登录用户的roles/permissions参数传入方法中

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

/**
 * @author zhaojinhui
 * @date 2020/12/8 22:56
 * @apiNote
 */
@SpringBootApplication
@MapperScan("com.javacode.mapper")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityApp {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApp.class,args);
    }
}
@GetMapping("/delete")
@PreAythorize("hasAnyAuthority('admin')")
public String update(){
    return "可以修改";
}

3. @PostAuthorize

使用前先开启注解@EnableGlobalMethodSecurity(prePostEnabled = true),在方法执行之后验证权限,适用于**验证角色是否有返回的权限**

@PostMapping("submit")
@PostAuthorize("hasAnyAuthority('admins')")
public String sumit(){
    return "没有返回权限";
}

4. @PostFilter

对方法返回的数据进行过滤,默认就是filterObject,不需要做出改变

@GetMapping("/getAll")
@PostAuthorize("hasAnyAuthority('admin')")
@PostFilter("filterObject.username == 'admin' ")
public List getAllUser(){
    User user2 = new User(“admin”,12);
    User user1 = new User(“admin1”,12);
    List<User> list = new ArrayList<>();
    Collections.addAll(list,user1,user2);
    return list;
}

5. @PreFilter

对方法传入的数据进行过滤

6. 权限表达式

9. 用户注销

在配置类中添加

package com.javacode.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.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;

/**
 * @author zhaojinhui
 * @date 2020/12/15 14:57
 * @apiNote
 */
@Configuration
public class WebSecurityConfig 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") //登录访问地址
            .defaultSuccessUrl("/test/index") //登录成功后的默认跳转路径
            .permitAll()
                //设置哪些路径可以直接访问不需要认证
            .and().authorizeRequests()
                .antMatchers("/","/test/hello")
                .permitAll()
                .antMatchers("/test/index").hasAuthority("admin")
                .antMatchers("/test/add").hasAnyRole("add")
            .and().csrf().disable()
            ;//关闭csrf防护
        //自定义403页面
        http.exceptionHandling().accessDeniedPage("/un_auth.html");
        //自定义注销页面
        http.logout().logoutUrl("/logout")
                .logoutSuccessUrl("/index")
                .permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


}

10.基于数据库的记住我功能

1.使用cookie

2.安全框架机制实现自动登录原理

第一步: 用户成功登陆之后往浏览器存储一个cookie加密串,然后同样的往数据库中存储包含有用户信息的加密串。

第二部:当用户再次访问的时候,先获取cookie信息用cookie信息到数据库中做对比,如果有对应的信息则认证成功可以登陆

记住我

3.具体实现

1. 创建数据库

如果我们不自己创建数据库的话,框架里有自带的建表语句

org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl 这个类里边是有的

-- 创建表
create table persistent_logins (
    username varchar(64) not null, 
    series varchar(64) primary key, 
    token varchar(64) not null, 
    last_used timestamp not null
);
2.修改配置类
  1. 首先需要注入数据源,配置对象
package com.javacode.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.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;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
 * @author zhaojinhui
 * @date 2020/12/15 14:57
 * @apiNote
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    
    /** 注入数据源 */
    @Autowired
    private DataSource dataSource;
    
    /** 配置对象*/
    @Bean
    public PersistentTokenRepository tokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //如果没有创建表的话需要开启下边的设置
        //jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
}    
  1. 添加记住我
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() //自定义自己编写的登录页面
        .loginPage("/login.html") //登录页面设置
        .loginProcessingUrl("/user/login") //登录访问地址
        .defaultSuccessUrl("/test/index") //登录成功后的默认跳转路径
        .permitAll()
        //设置哪些路径可以直接访问不需要认证
        .and().authorizeRequests()
        .antMatchers("/","/test/hello")
        .permitAll()
        .antMatchers("/test/index").hasAuthority("admin")
        .antMatchers("/test/add").hasAnyRole("add")
        .and().rememberMe() //设置记住我功能
        .tokenRepository(tokenRepository()) //设置操作token的对象
        .tokenValiditySeconds(86400 * 7) //记住七天,此方法以秒为单位
        .userDetailsService(userDetailsService) //操作数据库的对象
        .and().csrf().disable()
        ;//关闭csrf防护
    //自定义403页面
    http.exceptionHandling().accessDeniedPage("/un_auth.html");
    //自定义注销页面
    http.logout().logoutUrl("/logout")
        .logoutSuccessUrl("/index")
        .permitAll();
}
3. 前端页面修改

登录的地方添加内容,name的值必须是remember-me

<div>
    <label> 记住我:</label>
    <input type="checkbox" name="remember-me" title="七天免登陆"/>
</div>

4. 微服务权限方案

1. 什么是微服务

  1. 微服务的由来

微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

  1. 微服务优势

(1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。
(2)微服务每个模块都可以使用不同的存储方式(比如有的用xedis,有的用mysal.等),数据库也是单个模块对应自己的数据库。。
(3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。。

  1. 微服务本质

(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级。不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。

2. 在微服务中认证和授权的实现过程

1. SSO 单点登录

2. 授权登录

(1)如果是基于Session,那么Spring-security 会对cookie里的sessionid进行解析,找到服务器存储的session信息,然后判断当前用户是否符合请求的要求。
(2)如果是token,则是解析出token,然后将当前请求加入到Spring-security管理的权限信息中去。

token授权

3. 具体实现逻辑

  • 数据库设计
-- 权限信息表
CREATE TABLE `acl_permission` (
  `id` char(19) NOT NULL DEFAULT '' COMMENT '编号',
  `pid` char(19) NOT NULL DEFAULT '' COMMENT '所属上级',
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '名称',
  `type` tinyint(3) NOT NULL DEFAULT '0' COMMENT '类型(1:菜单,2:按钮)',
  `permission_value` varchar(50) DEFAULT NULL COMMENT '权限值',
  `path` varchar(100) DEFAULT NULL COMMENT '访问路径',
  `component` varchar(100) DEFAULT NULL COMMENT '组件路径',
  `icon` varchar(50) DEFAULT NULL COMMENT '图标',
  `status` tinyint(4) DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
  `gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
  `gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限';

-- 角色信息表
CREATE TABLE `acl_role` (
  `id` char(19) NOT NULL DEFAULT '' COMMENT '角色id',
  `role_name` varchar(20) NOT NULL DEFAULT '' COMMENT '角色名称',
  `role_code` varchar(20) DEFAULT NULL COMMENT '角色编码',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 角色权限表
CREATE TABLE `acl_role_permission` (
  `id` char(19) NOT NULL DEFAULT '',
  `role_id` char(19) NOT NULL DEFAULT '',
  `permission_id` char(19) NOT NULL DEFAULT '',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_role_id` (`role_id`),
  KEY `idx_permission_id` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限';

-- 用户信息表
CREATE TABLE `acl_user` (
  `id` char(19) NOT NULL COMMENT '会员id',
  `username` varchar(20) NOT NULL DEFAULT '' COMMENT '微信openid',
  `password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码',
  `nick_name` varchar(50) DEFAULT NULL COMMENT '昵称',
  `salt` varchar(255) DEFAULT NULL COMMENT '用户头像',
  `token` varchar(100) DEFAULT NULL COMMENT '用户签名',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 用户角色表
CREATE TABLE `acl_user_role` (
  `id` char(19) NOT NULL DEFAULT '' COMMENT '主键id',
  `role_id` char(19) NOT NULL DEFAULT '0' COMMENT '角色id',
  `user_id` char(19) NOT NULL DEFAULT '0' COMMENT '用户id',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_role_id` (`role_id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


1. 登录认证
  1. 登录过滤器
package com.eleven.filter;

import com.eleven.entity.SecurityUser;
import com.eleven.entity.User;
import com.eleven.security.TokenManager;
import com.eleven.util.ResponseUtil;
import com.eleven.util.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
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;
import java.util.HashMap;
import java.util.Map;

/**
 * @author zhaojinhui
 * @date 2021/1/14 23:29
 * @apiNote 登录过滤器
 */

public class TokenLoginFilter  extends UsernamePasswordAuthenticationFilter {

    private TokenManager tokenManager;

    private RedisTemplate redisTemplate;

    private AuthenticationManager authenticationManager;

    public TokenLoginFilter(TokenManager tokenManager,RedisTemplate redisTemplate,AuthenticationManager authenticationManager){
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.authenticationManager = authenticationManager;
        //关闭只允许post请求
        this.setPostOnly(false);
        //设置登录路径和提交方式
        this.setRequiresAuthenticationRequestMatcher(
                new AntPathRequestMatcher("/admin/acl/login","POST")
        );
    }


    /**
     * 获取表单提交过来的用户名和密码
     * @param request   http请求
     * @param response  http响应
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //获取表单提交过来的数据
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(),new ArrayList<>()));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("登录异常");
        }
    }

    /**
     * 认证成功之后调用的方法
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    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.getCurrentUserInfo().getUsername());
        //吧用户名称和权限信息放在redis token中
        redisTemplate.opsForValue()
                .set(user.getCurrentUserInfo().getUsername(), user.getPermissionList());
        //返回token
        Map map = new HashMap<>(1);
        map.put("token", token);
        ResponseUtil.out(response, new Result(map));
    }

    /**
     * 认证失败的调用的方法
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        ResponseUtil.out(response, new Result().error("认证失败"));
    }
}
  1. 用户退出并删除token
package com.eleven.security;

import cn.hutool.core.util.StrUtil;
import com.eleven.util.ResponseUtil;
import com.eleven.util.Result;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * @author zhaojinhui
 * @date 2021/1/14 23:03
 * @apiNote 用户退出并删除token信息
 */
public class TokenLogoutHandler implements LogoutHandler {

    @Autowired
    private RedisTemplate redisTemplate;

    private TokenManager tokenManager;


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

    /**
     * 用户退出
     * @param request 请求
     * @param response 响应
     * @param authentication
     */
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        //1.从header中获取token信息
        String token = request.getHeader("token");
        //2.如果token不为空 移除token。从redis中删除token
        if(StrUtil.isNotEmpty(token)){
            tokenManager.removeToken(token);

            //从token中获取用户信息
            String userName = tokenManager.getUserInfo(token);
            //从redis中删除用户信息
            redisTemplate.delete(userName);
        }

        ResponseUtil.out(response, new Result());
    }
}
2. 添加角色
3. 为角色分配菜单
4. 添加用户
5. 为用户分配角色

3. 完成基于Spring security认证授权案例

mplements LogoutHandler {

@Autowired
private RedisTemplate redisTemplate;

private TokenManager tokenManager;


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

/**
 * 用户退出
 * @param request 请求
 * @param response 响应
 * @param authentication
 */
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
    //1.从header中获取token信息
    String token = request.getHeader("token");
    //2.如果token不为空 移除token。从redis中删除token
    if(StrUtil.isNotEmpty(token)){
        tokenManager.removeToken(token);

        //从token中获取用户信息
        String userName = tokenManager.getUserInfo(token);
        //从redis中删除用户信息
        redisTemplate.delete(userName);
    }

    ResponseUtil.out(response, new Result());
}

}


#### 2. 添加角色

#### 3. 为角色分配菜单

#### 4. 添加用户

#### 5. 为用户分配角色

## 3. 完成基于Spring security认证授权案例

# 5. 原理总结
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值