Spring Security

14 篇文章 0 订阅
4 篇文章 0 订阅

Spring Security

spring系列的安全框架

与shiro比较优缺点

  • 缺点:
    • 配置非常繁琐,没有shiro简单
  • 优点:
    • 扩展性与自制性非常好
    • 作为Spring家族成员,与spring boot /cloud整合非常简单
    • 更够解决spring cloud的安全问题

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

笔记版本 SpringSecurity5.6.1 SpringBoot 2.6.2

Ⅰ 环境搭建

创建一个spring boot项目,导入security依赖,那么所有的项目接口就会受到security的保护

Ⅱ 基础知识点

介绍一些非代码性的知识点

1. 如何实现请求拦截

原生的请求拦截就是添加Filter ,security就是在原生的Filter链中加入了FilterChainProxy使得我们自定义的Filter会被执行

在这里插入图片描述

2. 过滤器链

Ⅲ SpringSecurity的默认认证配置

SpringSecurity是如何实现,只加入相关依赖就可以实现系统接口的所有认证但方式是默认的

1. 默认流程图

在这里插入图片描述

2. 默认配置

SpringBootWebSecurityConfiguration

SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    	//   开启认证			  所有请求		默认认证               表单认证            
		http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
		return http.build();
	}

默认装配生效有两个前提条件

// 有这两个类 则开启默认配置
@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
static class Classes {
}
// 没有这两个类 则开启默认配置
@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
static class Beans {

}

得出结论:

如果不想开启默认认证那就继承WebSecurityConfigurerAdapter或者SecurityFilterChain

3. 默认数据源装配

由SpringBoot提供的默认装配类实现

UserDetailsServiceAutoConfiguration

@Bean
@Lazy   // 内存user数据源管理器 是 UserDetailsService 的实现类
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
                                                             ObjectProvider<PasswordEncoder> passwordEncoder) {
    SecurityProperties.User user = properties.getUser();   // 从配置文件加载类中获取User数据
    List<String> roles = user.getRoles();
    return new InMemoryUserDetailsManager(	// 通过properties.getUser()获取的用户数据构建数据源 用户名、密码、角色
        User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
        .roles(StringUtils.toStringArray(roles)).build());
}

默认装配数据源生效条件

没有找到一下4个类型的类
    
@ConditionalOnMissingBean(value = 
                          { AuthenticationManager.class, AuthenticationProvider.class, 						 							 UserDetailsService.class,AuthenticationManagerResolver.class 
                          })

**得出结论:**若想自定义数据源,则可以实现其中任意一个接口

4. 默认认证流程

流程

  • UsernamePasswordAuthenticationFilter存在于SecurityFilterChain中,只要开启默认认证,认证请求就要经过这个类。UsernamePasswordAuthenticationFilter会在方法attemptAuthentication中完成认证

  • attemptAuthentication并不具备认证功能所以会调用ProviderManager完成认证

    • 在ProvicerManager处Security采用全局Manager+多子Manager策略也就是每一个ProvicerManager自身都会存储一个全局ProvicerManager,属性名为parent。当ProvicerManager自身无法完成认证时,会调用parent的认证方法完成认证。实际完成认证的不是ProviderManager,而是旗下所有的AuthenticationProvider
  • ProviderManager会调用所有的AuthenticationProvider进行认证,有一个不通过则认证失败

  • 默认流程中ProviderManager旗下只有一个AuthenticationProvider即DaoAuthenticationProvider

  • DaoAuthenticationProvider自身不具备数据源,它会调用UserDetailsService(接口)的实现类InMemoryUserDetailsManager获取数据源

    • DaoAuthenticationProvider认证成功后,一步步返回到UsernamePasswordAuthenticationFilter的调用者处,进行下一步
  • InMemoryUserDetailsManager中的user信息由UserDetailsServiceAutoConfiguration自动装配得来

  • UserDetailsServiceAutoConfiguration装配数据源从SecurityProperties中获取

5. 默认认证中涉及的重要成员
① AuthenticationManager、ProviderManager、AuthenticationProvider

在这里插入图片描述

  • AuthenticationManager 是认证管理器的顶级接口
  • ProviderManager是AuthenticationManager的一个实现
  • ProviderManager管理着多个AuthenticationProvider,在认证时会遍历AuthenticationProvider,有一个不通过则return false

ProviderManager与ProviderManager的关系

在这里插入图片描述

在ProvicerManager处,Security采用全局Manager+多子Manager策略也就是每一个ProvicerManager自身都会存储一个全局ProvicerManager,属性名为parent。当ProvicerManager自身无法完成认证时,会调用parent的认证方法完成认证。实际完成认证的不是ProviderManager,而是旗下所有的AuthenticationProvider

② WebSecurityConfigurerAdapter

如果我们写一个WebSecurityConfigurerAdapter的继承类,则Spring不会开启Security的默认认证

若想自定义Spring Security认证配置就需要写WebSecurityConfigurerAdapter的继承类

③ UserDetailsService

数据源的顶级接口,我们可以通过实现UserDetailsService来自定义数据源,

一但存在UserDetailsService的实现类,自动SpringBoot的自动装配也会失效,不再使用默认数据源

Ⅳ 自定义认证流程(未分离)

流程

  • 自定义拦截配置
  • 自定义登录页面以及路径
  • 自定义认证成功/失败相应
  • 自定义注销路径
  • 自定义数据源
  • 自定义验证码
1. 自定义拦截

在第Ⅲ部分得出,若要关闭SpringSecurity默认配置则需要继承WebSecurityConfigurerAdapter, SecurityFilterChain

以继承WebSecurityConfigurerAdapter为例

@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/index/** ").permitAll() // 拦截路径为/index/**的请求,并放行
                .anyRequest().authenticated()		// 拦截所有请求,并认证
                .and()
                .formLogin();  		// 认证方式为表单认证
    }
}

这样就可以实现基本的拦截

2. 自定义登录

在上述过程中自动跳转的登陆界面为Spring Security默认登录页

接下来自定以登录界面

步骤

  • 编写登陆界面
  • 根据登录界面内容设置,拦截认证行为
① 编写登陆界面

通过第Ⅲ部分源码分析,SpringSecurity默认情况下登录页面必须满足4个条件

  • 请求方式 method = “POST”
  • 请求路径 /login
  • 账号 name=“username”
  • 密码 name=“password”

这四个值,都可以自定义

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form th:action="@{/doLogin}" method="POST">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="submit"/>
</form>
</body>
</html>
② 修改拦截配置

步骤

  • 编写登陆页面跳转接口

  • 放行/login.html

  • 设置自定义登录页

  • 设置登录页请求路径(自定义登录页完成)

// 登陆页面跳转接口
@RequestMapping("/login.html")
public String login(){
    return "login";
}
http.authorizeRequests()
    .mvcMatchers("/index/**").permitAll()   
    .mvcMatchers("/login.html").permitAll()   // 放行登录页访问路径
    .anyRequest().authenticated()
    .and()
    .formLogin()
    .loginPage("/login.html") 				// 设置自定义登录页访问路径  必须与loginProcessingUrl一起使用
    .loginProcessingUrl("/doLogin")			// 设置需要捕获的登录请求(也就是登录页的登录请求) 必须与loginPage一起使用
    .and()
    .csrf().disable()    // 关闭远程请求保护
3. 自定义响应

响应有失败与成功,以及前后端分离/未分离

① 未分离

成功响应

  • successForwardUrl() 成功后显示的资源,不管你之前访问的什么资源,统一转发到该资源
  • defaultSuccessUrl() 成功后默认显示的资源,若之前有请求资源,则重定向到请求资源

二者不可同时使用

http.authorizeRequests()
    .mvcMatchers("/index/**").permitAll()   
    .mvcMatchers("/login.html").permitAll()   
    .anyRequest().authenticated()
    .and()
    .formLogin()
    .loginPage("/login.html") 				
    .loginProcessingUrl("/doLogin")		
    
    //.successForwardUrl("/hello")		// 设置登录成功后一定转发的路径
    .defaultSuccessUrl("/hello")		// 设置登录成功后默认重定向的路径,若访问其他资源则不跳转默认路径
    
    .and()
    .csrf().disable()  

失败响应

  • failureForwardUrl() 失败后转发到该资源,错误异常存放在request中
  • failureUrl() 失败后重定向到该资源,错误资源存放在session中

错误信息以 key-value 存储 key:“SPRING_SECURITY_LAST_EXCEPTION”

http.authorizeRequests()
    .mvcMatchers("/index/**").permitAll()
    .mvcMatchers("/login.html").permitAll()
    .anyRequest().authenticated()
    .and()
    .formLogin()
    .loginPage("/login.html")
    .loginProcessingUrl("/doLogin")

    .failureForwardUrl("/error.html")
    .failureUrl("/error.html")

    .and()
    .csrf().disable();
<h1 th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></h1>
<h1 th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></h1>
② 分离

成功响应

successHandler() 参数为AuthenticationSuccessHandler接口的实现类

在这里插入图片描述

Spring 默认帮我们实现了几个,当然也可以自定义

successForwardUrl -> ForwardAuthenticationSuccessHandler

defaultSuccessUrl -> SavedRequestAwareAuthenticationSuccessHandler

步骤

  • 自定义响应类
  • 修改拦截响应
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
       
    }
}
 http.authorizeRequests()
     .mvcMatchers("/index/**").permitAll()
     .mvcMatchers("/login.html").permitAll()
     .anyRequest().authenticated()
     .and()
     .formLogin()
     .loginPage("/login.html")
     .loginProcessingUrl("/doLogin")
     
     // 不同之处
     .successHandler(new MyAuthenticationSuccessHandler())
     
     .and()
     .csrf().disable()    // 关闭远程请求保护

失败响应
failureHandler()参数为AuthenticationFailureHandler实现类

在这里插入图片描述

步骤

  • 自定义响应类
  • 修改拦截响应
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code",200);
        result.put("success",true);
        result.put("authenticationInfo",authentication);
        String valueAsString = new ObjectMapper().writeValueAsString(result);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(valueAsString);
    }
}
 http.authorizeRequests()
     .mvcMatchers("/index/**").permitAll()
     .mvcMatchers("/login.html").permitAll()
     .anyRequest().authenticated()
     .and()
     .formLogin()
     .loginPage("/login.html")
     .loginProcessingUrl("/doLogin")
     
     // 不同之处
     .failureHandler(new MyAuthenticationFailureHandler())
     
     .and()
     .csrf().disable()    // 关闭远程请求保护
4. 自定义注销

默认实现

也就是Spring Security的默认实现

默认实现为发送 GET 方式的 /logout 请求,注销成功后自动跳转到登陆页面

在我们的拦截配置中加入
    .and()
    .logout()					// 开启自定义注销配置
    .logoutUrl("/logout")		// 注销请求路径,默认是GET方式
    .invalidateHttpSession(true)  // 清除session
    .clearAuthentication(true)	  // 清除认证信息

自定义注销

自定义则可以自己定义注销页面、路径、请求方式、注销成功后跳转的界面

.and()
    .logout()			// 开启自定义注销配置
    .logoutRequestMatcher(new OrRequestMatcher(   		// 指定匹配器  或类型,满足其中一个注销请求即可注销
        new AntPathRequestMatcher("/logout_get","GET"),  // get 注销
        new AntPathRequestMatcher("/logout_post","POST") // post 注销
    ))
    .invalidateHttpSession(true)		// 清除session
    .clearAuthentication(true)			// 清除认证信息
    .logoutSuccessUrl("/login.html")	// 设置注销后跳转的页面
5. 自定义数据源

在默认情况下,我们只能修改和使用SpringSecurity提供的默认数据源进行验证登录,为了实现真正的业务,必须自定义数据源

SpringSecurity提供了两种方式

① 修改默认的ProviderManager

这里SpringSecurity并没有直接为我们提供ProviderManager,而是给了构造器

@Autowired
public void initialize(AuthenticationManagerBuilder managerBuilder) throws Exception {
	// 在这里可以修改很多默认的ProviderManager配置
    // 修改数据源  这里模拟一下,创建一个InMemoryUserDetailsManager
    InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
    userDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin").build());
    managerBuilder.userDetailsService(userDetailsManager);
}

如果只修改数据源,我们可以简化一下代码

当Spring发现有自定义UserDetailsService时,会帮我自动注入

@Bean
public UserDetailsService userDetailsService(){
    InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
    userDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin").build());
    return userDetailsManager;
}
② 自定义ProviderManager

自定义ProviderManager会完全覆盖系统默认的ProviderManager。即便你在容器中加入了UserDetailsService实现类,也需要自己手动设置

实现

重写WebSecurityConfigurerAdapter中的configure(AuthenticationManagerBuilder auth)方法

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
    userDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin").build());
    auth.userDetailsService(userDetailsManager);
}
③ 自定义UserDetailsService

在自定义ProviderManger的基础上,自定义UserDetailsService,以及UserDetail

步骤:

  • 自定义UserDetail,编写用户数据源实体类User
  • 自定义UserDetailsService,注入UserMapper,并实现数据获取
  • 自定义ProviderManager,设置自定义的UserDetailsService
// 自定义UserDetail,编写用户数据源实体类User
/**
 * 实现UserDetail 添加必要字段,实现必要方法
 */
public class User implements UserDetails {

    // 主键
    private Integer id;
    // 密码 必要字段
    private String password;
    // 账号 必要字段
    private String username;
    // 账号是否过期 必要字段
    private boolean accountNonExpired;
    // 账号是否锁定 必要字段
    private boolean accountNonLocked;
    // 密码是否过期 必要字段
    private boolean credentialsNonExpired;
    // 是否启用 必要字段
    private boolean enabled;
    // 角色
    private Set<Role> roles;

    /**
     * 获取授权信息
     * 我们一般使用的是角色,需要转换一下
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : this.roles) {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
            authorities.add(simpleGrantedAuthority);
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

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

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

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

    public void setPassword(String password) {
        this.password = password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setAccountNonExpired(boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}
// 自定义UserDetailsService,注入UserMapper,并实现数据获取
@Component
public class MyUserDetailsServer implements UserDetailsService {

    // 注入属性
    @Autowired
    private UserMapper userMapper;

    /**
     * 实现用户信息加载方法
     * 设置数据源
     */ 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.getUserByUserName(username);
        if (ObjectUtil.isNull(user))
            throw new UsernameNotFoundException("账号未找到");
        List<Role> roles = userMapper.getRoles(user.getId());
        user.setRoles(new HashSet<>(roles));
        return user;
    }
}
// 自定义ProviderManager,设置自定义的UserDetailsService
@Autowired
private MyUserDetailsServer userDetailsServer;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsServer);
}
6. 自定义验证码

这个过程需要自定义过滤器,流程有点类似前后端分离

流程

  • 引入验证码生成依赖
  • 编写验证码配置类
  • 编写验证码获取接口
  • 自定义认证过滤器
  • 将自定义过滤器加入过滤器链
  • 设置过滤器必要属性
① 引入验证码生成依赖
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
② 编写验证码配置类
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
public class KaptchaConfig {

    @Bean
    public Producer producer() {
        Properties properties = new Properties();
        // 宽度
        properties.setProperty("kaptcha.image.width", "150");
        // 高
        properties.setProperty("kaptcha.image.height", "50");
        // 那些字符组成
        properties.setProperty("kaptcha.textproducer.char.string", "asdfghjkl");
        // 长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

}
③ 编写验证码获取接口
@GetMapping("/verCode")
public void getVerificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
    // 获取验证码文本
    String verCode = producer.createText();
    // 将验证码存入session,方便认证时获取  后期可放入redis中
    request.getSession().setAttribute("verCode",verCode);
    // 将文本生成图片
    BufferedImage producerImage = producer.createImage(verCode);
    // 设置相应格式
    response.setContentType(MediaType.IMAGE_PNG_VALUE);
    ServletOutputStream outputStream = response.getOutputStream();
    ImageIO.write(producerImage,"png",outputStream);

}
④ 自定义认证过滤器

自定义过滤器,我们只需要在官方认证过滤器之前加上验证码认证即可

public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final String VERIFICATION_CODE = "verCode";

    private String  verificationParameter = VERIFICATION_CODE;

    public String getVerificationParameter() {
        return verificationParameter;
    }

    public void setVerificationParameter(String verificationParameter) {
        this.verificationParameter = verificationParameter;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String verificationReq = request.getParameter(getVerificationParameter());
        String verCode = (String) request.getSession().getAttribute("verCode");
        if (!(StrUtil.isNotBlank(verificationReq)&&StrUtil.isNotBlank(verCode)&&verificationReq.equalsIgnoreCase(verCode))){
            throw new UsernameNotFoundException("验证码不正确");
        }
        return super.attemptAuthentication(request,response);
    }

}

⑤ 将自定义过滤器加入过滤器链

在configure中加入

http.addFilterAt(filter(), UsernamePasswordAuthenticationFilter.class);

暴露自定义AuthenticationManager

@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

设置过滤器属性

@Bean
public MyUsernamePasswordAuthenticationFilter filter() throws Exception {
    MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManagerBean());
    filter.setFilterProcessesUrl("/doLogin");
    filter.setUsernameParameter("name");
    filter.setPasswordParameter("password");
    filter.setAuthenticationSuccessHandler(((request, response, authentication) -> response.sendRedirect("/index")));
    filter.setAuthenticationFailureHandler(((request, response, authentication) -> response.sendRedirect("/error")));
    return filter;
}

注:必须在这里设置,configure的配置要整理掉

⑥ 设置过滤器必要属性

Ⅴ 自定义认证流程(分离)

认证流程中,分离与部分只有获取前端传递的认证参数的地方不同

  • 未分离 从request的请求体中获取username,password
  • 分离 从 request的请求体的Json中获取username,password

分析之后我们得出只要扩展 UsernamePasswordAuthenticaitonFilter 中参数获取方式即可

步骤

  • 自定义拦截配置类
  • 自定义扩展认证过滤器
  • 将自定义过滤器加入过滤器链
  • 自定义验证码校验
1. 自定义拦截配置类

实现内容与不分离不一样,需要添加一个没有认证时访问资源的异常处理

  • 不分离,在此情况下向客户返回登录页
  • 分离,在此情况下向前端返回需要认证json
@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
            
            	// 给前端相应一个 访问资源需要认证的异常处理
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> {
                    System.out.println(authException.getMessage());
                    response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
                    response.getWriter().print(authException.getMessage());
                })
            
                .and()
                .csrf().disable();
    }
}
2. 自定义扩展认证过滤器

继承UsernamePasswordAuthenticaitonFilter并扩展参数获取流程

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 判断是否是JSON格式,如果是进行处理 ,不是交给父类处理
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
            try {
                Map<String, String> loginInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                // 动态获取数据 ,方便后期修改维护,不写死
                String username = loginInfo.get(getUsernameParameter());
                String password = loginInfo.get(getPasswordParameter());
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
                this.setDetails(request, authenticationToken);
                return this.getAuthenticationManager().authenticate(authenticationToken);
            } catch (IOException e) {

                e.printStackTrace();
            }
        }
        return super.attemptAuthentication(request, response);
    }
}
3. 将自定义过滤器加入过滤器链

在将自定义的过滤器加入链之前,还需要一些必要操作

  • 自定义数据源,自定义认证管理器
  • 将过滤器加入容器中
  • 完善过滤器
  • 将过滤器加入过滤器链
① 自定义数据源,自定义认证管理器

自定义AuthenticationManager并且向外暴露,同时设置自定义数据源

// 自定义AuthenticationManager 并设置自定义数据源
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
    userDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin").build());
    auth.userDetailsService(userDetailsManager);
}

// 向外暴露AuthenticationManager
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}
② 将自定义过滤器加入容器中
@Bean
public MyUsernamePasswordAuthenticationFilter myFilter() throws Exception {
    MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();
    return filter;
}
③ 完善过滤器
  • 配置认证请求
  • 配置认证成功、失败响应
  • 配置注销响应
	// 设置登录路径 
    filter.setFilterProcessesUrl("/doLogin");
    // 设置认证成功结果
    filter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code",200);
        result.put("success",true);
        result.put("authenticationInfo",authentication);
        String valueAsString = new ObjectMapper().writeValueAsString(result);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(valueAsString);
    }));
    // 设置认证失败响应
	filte.setAuthenticationFaildHandler(((request,response,authentication)->{
        HashMap<String, Object> result = new HashMap<>();
        result.put("code",500);
        result.put("success",false);
        result.put("authenticationInfo",authentication);
        String valueAsString = new ObjectMapper().writeValueAsString(result);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(valueAsString);
    }));
    
    // 设置注销
     filter.setLogoutSuccessHandler((request,response,authentication)->{
        HashMap<String, Object> result = new HashMap<>();
        result.put("success",true);
        String valueAsString = new ObjectMapper().writeValueAsString(result);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(valueAsString);
     })
    // 将自定义AuthenticationManager设置到自定义过滤器中
    filter.setAuthenticationManager(authenticationManagerBean());

响应相关handler结构

在这里插入图片描述

④ 将自定义过滤器加入过滤器链
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .and()
        .csrf().disable();

    http.addFilterAt(myFilter(),UsernamePasswordAuthenticationFilter.class);
}
4. 自定义验证码校验

步骤

  • 添加验证码生成依赖
  • 编写验证码配置类
  • 编写验证码获取接口
  • 开放验证码接口
  • 修改过滤器,添加验证码校验
① 添加验证码生成依赖
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
② 编写验证码配置类
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
public class KaptchaConfig {

    @Bean
    public Producer producer() {
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "150");
        properties.setProperty("kaptcha.image.height", "50");
        properties.setProperty("kaptcha.textproducer.char.string", "asdfghjkl");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}
③ 编写验证码获取接口
@GetMapping("/verCode")
public String getVerCode(HttpServletRequest request) throws IOException {
    String verCode = producer.createText();
    request.getSession().setAttribute("verCode",verCode);
    BufferedImage image = producer.createImage(verCode);
    FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream();
    ImageIO.write(image,"png",outputStream);
    return Base64.encodeBase64String(outputStream.toByteArray());
}
④ 开放验证码接口
.mvcMatchers("/verCode").permitAll()
⑤ 修改过滤器,添加验证码校验

添加一下代码,部分省略

private final String VERIFICATION_CODE = "verCode";

private String  verificationParameter = VERIFICATION_CODE;

public String getVerificationParameter() {
    return verificationParameter;
}

public void setVerificationParameter(String verificationParameter) {
    this.verificationParameter = verificationParameter;
}

// 在重写的attemptAuthentication的用户密码封装之前添加一下代码
String verCode = userInfo.get(getVerificationParameter());
String verCodeReq = (String) request.getSession().getAttribute(getVerificationParameter());
if (!(StrUtil.isNotBlank(verCode)&&StrUtil.isNotBlank(verCode)&&verCode.equalsIgnoreCase(verCodeReq))){
    throw new UsernameNotFoundException("验证码错误");
}

Ⅵ 获取认证数据

通过代码方式获取一些SpringSecurity封装的数据,比如,认证信息、授权信息

1. 获取认证信息

在这里插入图片描述

重点

  • 我们可以通过SecurityContextHolder中的SecurityContextHolderStrategy成员属性获取用户数据

  • SecurityContextHolderStrategy有四种策略,常用三种

    • ThreadLocalSecurityContextHolderStrategy将用户数据保存在ThreadLocal中,在子线程中无法获取用户信息
    • InheritableThreadLocalSecurityContextHolderStrategy将用户数据保存在ThreadLocal中,在子线程中无法获取用户信息
    • GlobalSecurityContextHolderStrategy将用户数据存放在ApplicationContext中,太耗资源,不推荐
  • SecurityContextHolderStrategy的默认策略是ThreadLocalSecurityContextHolderStrategy

  • 通过源码发现,修改SecurityContextHolder的strategyName属性值可以达到修改策略的目的

    • private static String strategyName = System.getProperty("spring.security.strategy");
      
    • 我们需要修改系统属性,需要再jvm启动时修改参数,不能通过配置文件实现

使用代码获取用户认证信息

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 可以强转为我们使用的数据源对象

Ⅶ 密码加密/校验/更新

Security提供给我们很多加密方式,就是PasswordEncoder的实现类,可以直接使用

Spring Security的密码校验设计的非常巧妙

  • 如果容器中有指定加密方式,那么Security会直接使用指定加密方式进行校验**(不推荐)**
  • 如果没有,则会采用非常灵活的策略机制,从数据库密码中获取前缀,根据前缀匹配密码解析器
1. 密码加密

Security提供给我们很多加密方式,就是PasswordEncoder的实现类,可以直接使用

我们以BCrypt加密为例

采用更推荐的加密方式,拼接加密前缀

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);  // 参数为散列10次
String password = passwordEncoder.encode("123");
String encryptionPassword = "{bcrypt}"+password;   // 拼接加密前缀  其他加密前缀可以翻源码
System.out.println(encryptionPassword);
2. 密码校验一

直接指定项目加密方式,(不推荐,不灵活)

特点

  • 密码加密后不需要在密码中拼接加密前缀,也不能添加
  • 无法兼容之前数据库老密码

实现

在容器中添加对应的密码校验器就行

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
    return new BCryptPasswordEncoder();
}
3. 密码校验二

采用策略模式,根据密码前缀匹配密码校验器(Spring Security默认方式,推荐),Security自动对密码进行加密验证

特点

  • 可以兼容老密码
  • 加密方式可以多种多样
  • 需要在加密的密码前拼接加密前缀

Spring Security默认加密方式为 new BCryptPasswordEncoder(10),也就是散列次数为10

4. 密码自动更新升级

在认证成功后,security会判断容器中是否存在密码更新Bean(UserDetailsPasswordService),存在则会进行调用

  • 实现UserDetailsPasswordService,实现updatePassword方法
  • 参数newPassword,就是Security采用默认加密方式更新的密码(Security认为安全的)
  • 不能与密码校验一 一起使用
@Component
public class CustomUserDetailService implements UserDetailsService, UserDetailsPasswordService {

    private final UserMapper userMapper;
    
    @Autowired
    public CustomUserDetailService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.getUserByUserName(username);
        if (ObjectUtil.isNull(user))
            throw new UsernameNotFoundException("账号不存在");
        List<Role> roles = userMapper.getRoles(user.getId());
        user.setRoles(new HashSet<>(roles));
        return user;
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        // 调用数据库更新密码
        System.out.println(newPassword);
        // 设置新密码
        ((User) user).setPassword(newPassword);
        // 返回user,不然会报空指针
        return user;
    }
}

Ⅷ 记住我

1. 开启记住我

在我们的拦截配置中添加**rememberMe()**即可

   http.authorizeRequests()
       ...
       .and()
       .rememberMe()
	   ...

添加此方法后,过滤器链中的RememberMeAuthenticationFilter将会启用,拦截所有请求

若要实现记住我功能,请求需要携带参数 remember-me,值可以为 true/1/on/yes

2. 记住我实现流程

流程分为两部分生成登录令牌、自动登录

自动登录根据登录令牌实现自动登录

① 生成登录令牌

在系统开启自动登录,SecurityContextHolder中没有认证信息、cookie中没有登录令牌时会进入该流程(也就是第一次登录)

在这里插入图片描述

  • RememberMeAuthenticationFilter在各种条件都不满足的情况下放行
  • AbstractAuthenticationProcessingFilter拦截,并调用UsernamePasswordAuthenticationFilter的认证方法进行认证
  • 认证成功后AbstractAuthenticationProcessingFilter会调用认证成功的通知方法successfulAuthentication(…)
  • successfulAuthentication()方法会调用**AbstractRememberMeServices的loginSuccess()**进行记住我操作流程
  • loginSuccess()首先判断是否开启记住我流程,如果开启则会调用子类实现TokenBasedRememberMeServices的onLoginSuccess()完成最终操作
  • onLoginSuccess()中
    • 根据用户名、密码、签名到期时间以及Security提供的Key(一个UUID)生成一个字符串,再将字符串进行MD5加密生成用户签名
    • 再将用户名、签名到期时间、用户签名 经过Base64加密生成登陆令牌,并放入cookie中

以上就是登录令牌的生成流程

② 自动登录

该流程的核心是对登录令牌的检验

在这里插入图片描述

  • RememberMeAuthenticationFilter拦截请求,如果认证信息过期则会进行自动登录操作
  • RememberMeAuthenticationFilter调用**AbstractRememberMeServices的autoLogin()**进行自动登录
  • autoLogin()
    • 先判断cookie是否含有登录令牌
    • 再使用Base64对令牌解码,得到用户名、签名到期时间、签名
    • 最后调用子类实现**TokenBasedRememberMeServices的processAutoLoginCookie()**进行签名校验
  • processAutoLoginCookie()
    • 先判断是否过期
    • 再根据用户名获取密码
    • 最后使用 用户名、密码、签名到期时间 再使用MD5加密获取新的登录令牌,与cookie中的令牌进行比对
  • 自动登陆成功RememberMeAuthenticationFilter会更新认证信息、登录令牌
3. 安全性提高

现有问题

默认情况下的登录令牌非常不安全,无论你登录多少次,令牌都不会变化,一旦被劫取,就完了

如何升级

将令牌生成与校验的实现类TokenBasedRememberMeServices 修改为 PersistentTokenBasedRememberMeServices 即可

区别就是PersistentTokenBasedRememberMeServices每次访问后都会更新令牌,并且令牌不再携带用户名

如何实现

创建PersistentTokenBasedRememberMeServices实体类到容器中

@Bean
public PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices(){
    return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),userDetailsServer,new 			InMemoryTokenRepositoryImpl());
}

参数:key 用户信息 令牌存放位置(security提供了内存存储)
4. 前后端不分离实现

在后端开启记住我,在前端页面添加check

<input type="checkbox" value="1">   1/yes/on/true
5. 前后端分离实现

分析源码得

  • AbstractRememberMeServices中的loginSuccess方法会去判断是否开启remember

  • 判断方式为从Request请求体中获取remember-me参数,判断值是否为true/on/1/yes

  • 由上可得,我们只需要将其获取remember-me参数的地方,改为json获取即可完成前后端分离记住我

  • 考虑到我们在认证时已经解析Request的输入流,所以我们可以在那时将remember-me参数解析,并存入request域中

  • 在我们修改的remember-me判断处,将其取出

实现步骤

  • 在自定义的UsernamePasswordAuthenticationFilter中将参数remember-me存入request域中
  • 继承AbstractRememberMeServices的子类,并重写判断方法rememberMeRequested(remember-me)
  • 将自定义的AbstractRememberMeServices设置给认证拦截器
  • 将自定义的AbstractRememberMeServices设置给记住我拦截器
① 获取remember-me

在自定义的UsernamePasswordAuthenticationFilter中将参数remember-me存入request域中

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    ...
        String rememberValue = loginInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
    	// 防止后续空指针异常
        if (StrUtil.isNotBlank(rememberValue)){
            request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER,rememberValue);
        }else {
            request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER,"");
        }
    ...
}
 
② 自定义AbstractRememberMeServices

选择继承实现类PersistentTokenBasedRememberMeServices

import org.springframework.core.log.LogMessage;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.servlet.http.HttpServletRequest;

public class MyRememberMeServices extends PersistentTokenBasedRememberMeServices {

    public MyRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService, tokenRepository);
    }

    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        // 只修改这一句
        String paramValue = request.getAttribute(parameter).toString();
        
        if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
            this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
            return false;
        } else {
            return true;
        }
    }
}

③ 将自定义的AbstractRememberMeServices设置给认证拦截器

这是最关键的一步,让拦截器使用我们定义的类

@Bean
public MyRememberMeServices myRememberMeServices(){
    return new MyRememberMeServices(UUID.randomUUID().toString(),userDetailsService(),new InMemoryTokenRepositoryImpl());
}

// 在自定义的认证过滤器中设置
@Bean
public MyUsernamePasswordAuthenticationFilter myFilter() throws Exception {
 	...
        filter.setRememberMeServices(myRememberMeServices()); 
    ...
}
④ 将自定义的AbstractRememberMeServices设置给记住我拦截器

因为我们继承的并不是Spring Security提供的默认rememberMeService,所以在自动登录的地方也要设置一下

@Override
protected void configure(HttpSecurity http) throws Exception {
 	...
        .rememberMe()
        .rememberMeServices(myRememberMeServices())
    ...
}

Ⅸ 会话管理

会话也就是session管理

本节涉及:

  • session并发管理
  • session共享
1. 并发处理

session并发,也就是允许一个账号在几个平台同时登录

① 开启并发管理

在这里我们以session并发2为例

省略security前戏

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .mvcMatchers("/index").permitAll()
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .and()
        .csrf().disable()
        .sessionManagement()
        .maximumSessions(1);   // 设置session最大并发量
}
② 设置session并发响应

当session并发超过,我们需要通知用户

前后端未分离

添加配置项
...
.sessionManagement()
.maximumSessions(1)
.expiredUrl("/expired.html")     // 设置要跳转的页面
...

前后端分离

实现与前后端分离认证失败等类似

...
.sessionManagement()
.maximumSessions(1)
.expiredSessionStrategy(event -> {
    HttpServletResponse response = event.getResponse();
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    HashMap<String, Object> hashMap = new HashMap<>();
    hashMap.put("code",500);
    hashMap.put("message","其他平台已登陆");
    String string = new ObjectMapper().writeValueAsString(hashMap);
    response.getWriter().println(string);
    response.flushBuffer();
});
...
③ 禁止登录设置

当session超过最大并发量,则禁止客户端登录

在配置项中添加是否可登录

...
    .sessionManagement()
    .maximumSessions(1)
    .maxSessionsPreventsLogin(true)
...
2. 会话共享

当我们应用是集群的时候,以上操作的session并发将会失效。因为多服务器之间的session并不共享,所以需要借助外物,做到会话共享共享

方案是将数据存储到Redis中

实现步骤

  • 导入相关依赖
  • 配置redis配置
  • 在配置项中设置session注册地址
  • 测试
① 导入相关依赖
redis依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis-session相关依赖
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
② 配置redis配置
spring:
   redis:
      username: root
      url: redis://@192.168.37.100:6379
③在配置项中设置注册地址

第一步创建session-redis注册器

private final FindByIndexNameSessionRepository repository;

@Autowired
public SecurityConfig(FindByIndexNameSessionRepository repository) {
    this.repository = repository;
}

第二步创建security-session注册器

@Bean
public SpringSessionBackedSessionRegistry springSessionBackedSessionRegistry(){
	return new SpringSessionBackedSessionRegistry(repository);
}

第三步在配置项中配置注册器

...
    .maximumSessions(1)
    .maxSessionsPreventsLogin(true)
    .sessionRegistry(springSessionBackedSessionRegistry()) 
...
④ 测试

将同一个程序启动多个服务

  • 复制当前服务
  • 在 VM Options设置参数 -Dserver.port=8081
  • 启动双服务在多平台测试

Ⅹ CSRF/CORS

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

1. Security防御CSRF

采取在提交是添加_csrf参数

<input type="hidden" name="_csrf" value="3cc8e049-9026-44c5-8a8c-339e75aad15b">

通过令牌方式防御

2. 防御CSRF
① 不分离

传统项目很简单,直接在认证配置项中开启防御CSRF即可。

开启之后Spring Security会自动在登录页中添加 _csrf输入框

...
   .csrf() 
...
分离
3. CORS跨域问题解决
① SpringMVC解决方法

注解解决

直接在接口上添加跨域注解

@CrossOrigin

MVC全局解决

实现mvc全局配置接口

配置跨域相关信息

@Configuration
public class MyMVCConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")  		  // 对所有请求生效
                .allowCredentials(false)      // 不需要凭证
                .allowedHeaders("*")          // 任何头都可以
                .allowedMethods("*")          // 任何方法
                .allowedOrigins("*")          // 任何域
                .maxAge(3600);
    }
}
4. Security解决

因为Security过滤器的优先级高于MVC,所以当我们开启Security的跨域配置后,MVC的跨域配置将不再生效

一般使用MVC的就行

security跨域配置实现

// 添加配置项,开启跨域配置
...
	 .and()
     .cors()
     .configurationSource(corsConfigurationSource())
...


// 设置跨域参数
 CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",configuration);
        return urlBasedCorsConfigurationSource;
    }

ⅩⅠ 异常处理

Security中的异常一共有两大类

  • AuthenticationException 认证异常
  • AccessDeniedException 授权异常

每一种的子实现都很多,比如:用户锁定一场、凭证过期异常等等…

1. 默认实现

在Security中,

  • 认证异常默认跳转到登陆页面
  • 授权默认抛出异常
2. 自定义异常

直接在配置项中添加配置即可

...
 	.and()
    .exceptionHandling()      // 开启异常处理
    .authenticationEntryPoint(((request, response, authException) -> {			// 认证异常处理
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().println(authException.getMessage());
    }))
    .accessDeniedHandler(((request, response, accessDeniedException) -> {		// 授权异常处理
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().println(accessDeniedException.getMessage());
    }))
...

ⅩⅡ 授权管理

这一节盘点安全框架的另一大核心,授权

1. 前提知识

说一些不是授权知识但是授权基础,比如说:什么是授权、什么是权限、授权需要信息在哪里等

① 什么授权

概念

​ 在项目中通常会为资源添加一些权限。也就是说虽然用户进行了认证,但是你可能没有资格访问某些资源,比如说VIP用户于普通用户。所以需要一些授权进行判断。

一般情况下对资源授权分类两种 角色、权限

  • 如果项目是角色+资源的方式,那么一个角色通常对应很多资源,需要验证用户是否拥有某角色
  • 如果项目是权限+资源方式,那么就需要验证用户是否拥有该权限
  • 如果项目是角色+权限+资源方式,那么就是一个角色对应多个权限,一个权限对应多个资源

举例

  • 角色+权限+资源
    • 一个添加权限可以调用所有添加接口,一个经理角色拥有添加权限、修改权限
② 授权需要的信息在哪里

Security在认证之后会有一个认证对象Authentication

public interface Authentication extends Principal, Serializable {
    
    // 角色集合或者是权限集合  根据项目返回
    Collection<? extends GrantedAuthority> getAuthorities();

    // 获取认证凭证
    Object getCredentials();

    // 获取用户信息
    Object getDetails();

    // 
    Object getPrincipal();

    // 是否认证成功
    boolean isAuthenticated();

    // 设置认证是否成功
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
③ Security的授权策略

security提供了两种授权策略**(httpUrl,method)** url授权和方法授权

具体实现

  • 基于过滤器的权限管理(FilterSecurityInterceptor)实现对httpUrl的授权
    • 拦截http请求,进行授权
  • 基于AOP的权限管理(MethodSecurityInterceptor)实现对method的授权
    • 对method添加aop操作实现拦截

注:在Security授权中,角色授权会自动加上ROLE_前缀,资源授权需要手动加上READ_

2. URL授权
① 简单授权配置

直接在配置项中添加请求拦截情况

 @Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .mvcMatchers("/admin").hasRole("admin")   // 表示请求该url需要拥有角色 ROLE_admin
        .mvcMatchers("/index").hasAuthority("READ_index")  
        .anyRequest()
        .authenticated()
        .and()
        .formLogin()
        .and()
        .csrf().disable();

}

表示请求 /admin 需要拥有角色 ROLE_admin

表示请求 /index 需要拥有权限 READ_index

② 路径匹配方法
  • antMatchers() 最基础url匹配方法 , /admin 只能匹配 /admin 的请求
  • mvcMatchers() 是antMatchers的升级版,/admin 可以匹配 /admin.html,可以使用通配符
  • regexMatchers() 可以使用正则表达式

路径匹配方法的后续方法

  • hasRole(role) 需要有指定的角色
  • hasAnyRole(role…) 有任一角色即可
  • hasAuthority(权限)需要有指定权限
  • hasAnyAuthority(权限…)有任一权限即可
  • permitAll()什么都不需要
3. Method授权

方法授权更多的是在方法上加注解,通过注解来配置具体授权

MethodSecurityInterceptor其实是一个切面,方法授权就是给方法加了aop,在之前或者之后做一些事

常用注解

  • PreAuthorize()
  • PostAuthorize()
  • PreFilter()
  • PostFilter()

以上注解都支持 权限表达式,能写的内容非常多

① 开启注解支持

在Spring Boot启动类上添加开启直接

@EnableGlobalMethodSecurity(prePostEnabled = true)
② PreAuthorize

前置认证方法,在请求进入方法之前进行一些授权判断

// 需要有admin角色
@PreAuthorize("hasRole('admin')")
// 需要有admin角色,并且登录的用户名为 llz
@PreAuthorize("hasRole('admin') and authentication.name == 'llz'")
// 有任意一个角色即可
@PreAuthorize("hasAnyRole('admin','manager')")
// 登录的用户名需要与参数name一致
@PreAuthorize("authentication.name == #name")
③ PostAuthorize

后置授权注解

④ PreFilter

前置过滤注解

⑤ PostFilter

后置过滤注解

4. 授权原理分析

在这里插入图片描述

  • 请求被FilterSecurityInterceptor过滤器拦截,doFilter方法调用父类AbstractSecurityInterceptor的beforeInvocation(url)方法完成授权,并返回授权token。
  • beforeInvocation方法
    • 通过参数中的url从FilterInvocationSecurityMetadataSource的getAttributes(obj)方法中获取该url上的授权条件
    • 调用自身attemptAuthorization方法进行授权认证。
  • attemptAuthorization中调用自身的AccessDecisionManager的decide方法进行授权。
  • decide方法会遍历与该授权相关的AccessDecisionVoter一次进行授权判断

分析源码得出,如果我们想要把数据库中的授权配置信息放入Security(动态配置授权),那么就需要实现FilterInvocationSecurityMetadataSource接口,并且重写getAttributes(obj)方法

5. 动态设置授权配置信息

也就是说路径的授权权限不再写死,而是从数据库中获取

分析源码可得,我们需要实现FilterInvocationSecurityMetadataSource接口重写getAttributes(obj)

实现步骤

  • 编写Menu以及Role实体类

  • 编写Menu的Mapper

  • 自定义授权资源源

  • 将自定义的授权资源源交给Security

  • 配置用户信息

① 编写Menu以及Role实体类

简单起见并没有设置复杂的Menu以及Role

roles为该资源路径通过授权需要的角色

@Data
public class Menu {

    private Integer id;

    private String url;

    private List<Role> roles;
}

@Data
public class Role {

    private Integer id;

    private String name;
}
② 编写Menu的Mapper

就是查出所有的Menu对象,并且包含自己授权通过所需的Role

<mapper namespace="com.example.security05authorisation.mapper.MenuMapper">
    <resultMap id="menu" type="com.example.security05authorisation.entity.Menu">
        <id property="id" column="id"/>
        <result property="url" column="url"/>
        <collection property="roles" ofType="com.example.security05authorisation.entity.Role">
            <id property="id" column="rid"/>
            <result property="name" column="name"/>
        </collection>
    </resultMap>

    <select id="getMenuAll" resultMap="menu">
        SELECT m.id,m.url,r.id rid,r.`name`
        FROM `menu` m
        LEFT JOIN role_menu rm ON m.id=rm.menu_id
        LEFT JOIN role r ON r.id=rm.role_id
    </select>
</mapper>
③ 自定义授权资源源

实现FilterInvocationSecurityMetaSource接口

这个方法最关键的就是获取到该资源路径授权通过所需要的Role列表,不管角色菜单设计的如何,最后要的就是这个东西,将角色列表交给Security创建出Collection即可

package com.example.security05authorisation.config;


import com.example.security05authorisation.entity.Menu;
import com.example.security05authorisation.entity.Role;
import com.example.security05authorisation.mapper.MenuMapper;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

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

@Component
public class CustomSecurityMetaSource implements FilterInvocationSecurityMetadataSource {

    // 路径比较器,比如“/admin/**”与“/admin/a”也是能通过的
    AntPathMatcher matcher = new AntPathMatcher();

    // 菜单的menuMapper 用来获取菜单资源
    private final MenuMapper mapper;

    public CustomSecurityMetaSource(MenuMapper mapper) {
        this.mapper = mapper;
    }

	/**
	 * 实现方法,主要就是该方法了
	 */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        // 获取请求url
        String requestUrl = ((FilterInvocation)object).getRequestUrl();
        // 获取所有的菜单以及菜单所需角色
        List<Menu> menuList = mapper.getMenuAll();
        for (Menu menu : menuList) {
            // 找到对应的菜单
            if(matcher.match(menu.getUrl(),requestUrl)){
                // 最关键的一步将该资源路径需要所有角色名称放在一个数组中
                // 将菜单角色列表转为数组,注意`ROLE_`前缀的坑,用户信息那边不加,这边也不加
                String[] roles = menu.getRoles().stream()
                    .map(p->"ROLE_"+p.getName()).toArray(String[]::new);
                // 交由SecurityConfig(Spring)创建Collection<ConfigAttribute>
                return SecurityConfig.createList(roles);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
④ 将自定义的授权资源源交给Security

这一步在public class SecurityConfig extends WebSecurityConfigurerAdapter{}中完成

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomSecurityMetaSource metaSource;

    public SecurityConfig(CustomSecurityMetaSource metaSource) {
        this.metaSource = metaSource;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 将自定义的授权信息对象交给Security
        ApplicationContext context = http.getSharedObject(ApplicationContext.class);
        http.apply(new UrlAuthorizationConfigurer<>(context))
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        // 交给Security
                        object.setSecurityMetadataSource(metaSource);
                        // 是否拒绝没有配置授权信息的url的请求,改为false
                        object.setRejectPublicInvocations(false);
                        return object;
                    }
                });

        // 如果配置授权源,那就不用在这里配置拦截情况了
        http.formLogin().and().csrf().disable();
    }
}
⑤ 配置用户信息

为了省事就是用了内存用户信息

   // 为了省事使用内存用户信息
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin","class").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lzz").password("{noop}123").roles("manager").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("zzz").password("{noop}123").roles("class").build());
        return inMemoryUserDetailsManager;
    }

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

ⅩⅢ OAuth2.0

1. 简介

OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方 应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他 们数据的所有内容。OAuth在全世界得到广泛应用,目前的版本是2.0版。

协议特点:

  • 简单:不管是OAuth服务提供者还是应用开发者,都很易于理解与使用;
  • 安全:没有涉及到用户密钥等信息,更安全更灵活;
  • 开放:任何服务提供商都可以实现OAuth,任何软件开发商都可以使用OAuth

应用场景

  • 原生app授权:app登录请求后台接口,为了安全认证,所有请求都带token信息,如果登录验证、 请求后台数据。
  • 前后端分离单页面应用:前后端分离框架,前端请求后台数据,需要进行oauth2安全认证
  • 第三方应用授权登录,比如QQ,微博,微信的授权登录。

优缺点

优点:

  • 更安全,客户端不接触用户密码,服务器端更易集中保护
  • 广泛传播并被持续采用
  • 短寿命和封装的token
  • 资源服务器和授权服务器解耦
  • 集中式授权,简化客户端
  • HTTP/JSON友好,易于请求和传递token
  • 考虑多种客户端架构场景
  • 客户可以具有不同的信任级别

缺点:

  • 协议框架太宽泛,造成各种实现的兼容性和互操作性差
  • 不是一个认证协议,本身并不能告诉你任何用户信息

Security http) throws Exception {
// 将自定义的授权信息对象交给Security
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
http.apply(new UrlAuthorizationConfigurer<>(context))
.withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public O postProcess(O object) {
// 交给Security
object.setSecurityMetadataSource(metaSource);
// 是否拒绝没有配置授权信息的url的请求,改为false
object.setRejectPublicInvocations(false);
return object;
}
});

    // 如果配置授权源,那就不用在这里配置拦截情况了
    http.formLogin().and().csrf().disable();
}

}




##### ⑤ 配置用户信息

为了省事就是用了内存用户信息

```java
   // 为了省事使用内存用户信息
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin","class").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lzz").password("{noop}123").roles("manager").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("zzz").password("{noop}123").roles("class").build());
        return inMemoryUserDetailsManager;
    }

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

ⅩⅢ OAuth2.0

1. 简介

OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方 应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他 们数据的所有内容。OAuth在全世界得到广泛应用,目前的版本是2.0版。

协议特点:

  • 简单:不管是OAuth服务提供者还是应用开发者,都很易于理解与使用;
  • 安全:没有涉及到用户密钥等信息,更安全更灵活;
  • 开放:任何服务提供商都可以实现OAuth,任何软件开发商都可以使用OAuth

应用场景

  • 原生app授权:app登录请求后台接口,为了安全认证,所有请求都带token信息,如果登录验证、 请求后台数据。
  • 前后端分离单页面应用:前后端分离框架,前端请求后台数据,需要进行oauth2安全认证
  • 第三方应用授权登录,比如QQ,微博,微信的授权登录。

优缺点

优点:

  • 更安全,客户端不接触用户密码,服务器端更易集中保护
  • 广泛传播并被持续采用
  • 短寿命和封装的token
  • 资源服务器和授权服务器解耦
  • 集中式授权,简化客户端
  • HTTP/JSON友好,易于请求和传递token
  • 考虑多种客户端架构场景
  • 客户可以具有不同的信任级别

缺点:

  • 协议框架太宽泛,造成各种实现的兼容性和互操作性差
  • 不是一个认证协议,本身并不能告诉你任何用户信息
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值