1、SpringSecurity概述

1、概要

  1. Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实上的标准。<br />Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。与所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义要求

1、特点

  • 对身份验证和授权的全面且可扩展的支持
  • 防止会话固定、点击劫持、跨站点请求伪造等攻击
  • 与 Spring Web MVC 的可选集成

    2、用户认证

  • 验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过检验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录

    3、用户授权

  • 验证某个用户是否有权限执行某个操作。在一个系统中不同用户所具有的权限是不同的。通俗点讲就是系统怕暖用户是否有权限去做某些事情

    2、历史

    Spring Security 开始于2003年年底,Spring的acegi安全系统,经过迭代,最终2007年改为Spring组合项目,更名为Spring Security

    3、产品对比

    1、Spring Security

  • 优点:

    • Spring技术栈和Spring 无缝整合
    • 全面的权限控制
    • 专门为Web开发而设计
    • 旧版本不能脱离Web环境使用
    • 新版本对整个框架进行了分层抽取,分成了核心模块和Web模块。单独引入核心模块就可以脱离Web环境
  • 缺点:

    • 重量级

      2、Shiro

  • 优点

    • 轻量级,Shiro主张的理念把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现
    • 通用性,不局限于Web环境,可以脱离Web环境使用
  • 缺点:

    • 在Web环境下一些特定的需求需要手动编写代码定制

      3、总结:

      在Spring Boot出现之前,SpringSecurity已经发展多年。但是使用的并不多,安全管理这个领域一直是Shiro的天下。相对于Shiro,在SSM中整合Spring Security都是比较麻烦的操作,所以Spring Security虽然功能比Shiro强大,但是使用反而没有Shiro多(虽然Shiro功能没有Spring Security多,但是对于大部分项目而言,Shiro也够用了)。
      自从有了Spring Boot之后,Spring boot对于Spring Securty提供了自动化配置方案,一般来说,常见的安全管理技术栈的组合是这样的:
  • SSM+Shiro

  • Spring Boot/Spring Cloud+Spring Security

以上只是一个推荐的组合而已,单纯从技术上来说,无论怎么组合,都是可以运行的。

4、模块

  • https://docs.spring.io/spring-security/reference/modules.html

    1、核心 — spring-security-core.jar

    该模块包含核心身份验证和访问控制类和接口、远程支持和基本供应 API。任何使用 Spring Security 的应用程序都需要它。它支持独立应用程序、远程客户端、方法(服务层)安全性和 JDBC 用户供应。它包含以下顶级包

  • org.springframework.security.core

  • org.springframework.security.access
  • org.springframework.security.authentication
  • org.springframework.security.provisioning

    1、核心依赖

  • ehcache:如果使用基于Ehcache的用户缓存实现,则需要(可选)

  • spring-aop:防范安全基于Spring AOP
  • spring-beans:Spring 配置所需
  • spring-expression:基于表达式的方法安全性是必需的(可选)
  • spring-jdbc:如果使用数据库来存储用户数据,则需要(可选)
  • spring-tx:如果使用数据库来存储用户数据,则需要(可选)
  • aspectjrt:如果使用AspectJ支持,则需要(可选)
  • jsr250-api:如果您使用JSR-250方法安全注释(可选),则为必需。

    2、远程—— spring-security-remoting.jar

    该模块提供与 Spring Remoting 的集成。除非您正在编写使用 Spring Remoting 的远程客户端,否则您不需要它。主要包是

  • org.springframework.security.remoting.

    3、网络— spring-security-web.jar

    该模块包含过滤器和相关的网络安全基础设施代码。它包含任何具有 servlet API 依赖项的内容。如果您需要 Spring Security Web 身份验证服务和基于 URL 的访问控制,则需要它。主要包是

  • org.springframework.security.web.

    1、核心依赖

  • spring-web:Spring Web支持类被广泛使用

  • spring-jdbc:基于JDBC的持久记住我令牌存储库是必须的(可选)
  • spring-tx:记住我的持久令牌存储库实现需要(可选)。

    4、配置— spring-security-config.jar

    该模块包含安全命名空间解析代码和 Java 配置代码。如果您使用 Spring Security XML 命名空间进行配置或 Spring Security 的 Java 配置支持,则需要它。没有一个类旨在直接在应用程序中使用。主要包含

  • org.springframework.security.config.

    5、LDAP — spring-security-ldap.jar

    此模块提供 LDAP 身份验证和供应代码。如果您需要使用 LDAP 身份验证或管理 LDAP 用户条目,则需要它。顶级包是org.springframework.security.ldap.

    6、OAuth 2.0 核心 — spring-security-oauth2-core.jar

    spring-security-oauth2-core.jar包含为 OAuth 2.0 授权框架和 OpenID Connect Core 1.0 提供支持的核心类和接口。使用 OAuth 2.0 或 OpenID Connect Core 1.0 的应用程序需要它,例如客户端、资源服务器和授权服务器。顶级包是org.springframework.security.oauth2.core.

    7、OAuth 2.0 客户端 — spring-security-oauth2-client.jar

    spring-security-oauth2-client.jar包含 Spring Security 对 OAuth 2.0 授权框架和 OpenID Connect Core 1.0 的客户端支持。使用 OAuth 2.0 登录或 OAuth 客户端支持的应用程序需要它。顶级包是org.springframework.security.oauth2.client.

    8、OAuth 2.0 JOSE — spring-security-oauth2-jose.jar

    spring-security-oauth2-jose.jar包含 Spring Security 对 JOSE(Javascript 对象签名和加密)框架的支持。JOSE 框架旨在提供一种在各方之间安全地传输声明的方法。它由一系列规范构建而成:

  • JSON 网络令牌 (JWT)

  • JSON 网络签名 (JWS)
  • JSON 网络加密 (JWE)
  • JSON 网络密钥 (JWK)

    9、OAuth 2.0 资源服务器 — spring-security-oauth2-resource-server.jar

    spring-security-oauth2-resource-server.jar包含 Spring Security 对 OAuth 2.0 资源服务器的支持。它用于通过 OAuth 2.0 Bearer Tokens 保护 API。顶级包是org.springframework.security.oauth2.server.resource.

    10、ACL— spring-security-acl.jar

    该模块包含一个专门的域对象 ACL 实现。它用于将安全性应用于应用程序中的特定域对象实例。顶级包是org.springframework.security.acls

    11、CAS— spring-security-cas.jar

    该模块包含 Spring Security 的 CAS 客户端集成。如果要将 Spring Security Web 身份验证与 CAS 单点登录服务器一起使用,则应该使用它。顶级包是org.springframework.security.cas.

    12、OpenID — spring-security-openid.jar

  • OpenID 1.0 和 2.0 协议已被弃用,鼓励用户迁移到 Spring-security-oauth2 支持的 OpenID Connect。

此模块包含 OpenID Web 身份验证支持。它用于针对外部 OpenID 服务器对用户进行身份验证。顶级包是org.springframework.security.openid. 它需要 OpenID4Java。

2、Spring Security入门案例

1、创建SpringBoot工程

  • 通过maven快捷创建一个项目
  • 通过maven-archetype-quickstart创建

image.png

2、编写pom.xml

  • 去除多余的pom文件中的内容
  • 编写新的依赖
  • 主要是3个2个依赖一个是
    • spring-boot-starter-web
    • spring-boot-starter-security ```xml <?xml version=”1.0” encoding=”UTF-8”?>
4.0.0 com.daijunyi security 1.0-SNAPSHOT security org.springframework.boot spring-boot-starter-parent 2.2.1.RELEASE UTF-8 1.8 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-maven-plugin
<a name="hNQGL"></a>
## 3、编写主启动类

- 主启动类
- pom.xml
```java
package com.daijunyi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author djy
 * @createTime 2021/12/13 上午6:28
 * @description
 */
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }
}

4、编写配置类

  • SecurityConfig ```java package com.daijunyi.config;

import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**

  • @author djy
  • @createTime 2021/12/13 上午7:26
  • @description */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
    //表单登录
    http.formLogin()
            .and()
            //认证配置
            .authorizeRequests()
            //任何请求
            .anyRequest()
            //都需要身份验证
            .authenticated();
}

}

<a name="ImWOo"></a>
## 5、编写yaml

- application.yaml
```yaml
server:
  port: 8081

6、编写controller

  • HelloController ```java package com.daijunyi.controller;

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

/**

  • @author djy
  • @createTime 2021/12/13 上午6:31
  • @description */ @RestController @RequestMapping(“/hello”) public class HelloController {

    @GetMapping(“/security”) public String saySecurity(){

     return "hello security";
    

    }

}

<a name="Ulfts"></a>
## 7、测试

- 启动[http://localhost:8081/hello/security](http://localhost:8081/hello/security)
- 自动跳入![image.png](https://cdn.nlark.com/yuque/0/2021/png/12971636/1639348880166-d26cecb2-ee71-4de2-a64b-e4d3cb190a9e.png#clientId=u0c8dac23-c9c7-4&from=paste&height=329&id=u56a6acf0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=658&originWidth=1718&originalType=binary&ratio=1&size=49732&status=done&style=none&taskId=ue2f629a6-7b74-4eef-8b28-0f0ed68756e&width=859)
- 登录
   - 用户名是user
   - 密码是项目启动的时候控制台会自动打印一个密码
```shell
Using generated security password: ae1971d8-f1cd-47bf-b88d-786274babbf4
  • 输入之后就自动登录了

    3、spring Security基本原理

  • Spring Security本质是一个过滤器链

    • 有很多过滤器

      1、所有过滤器

  • ChannelProcessingFilter

    • 使用https还是http的通过过滤器
  • WebAsyncManagerIntegrationFilter
    • 此过滤器使得WebAsync异步线程能够获取到当前认证信息
  • SecurityContextPersistenceFilter
    • 主要控制 SecurityContext 的在一次请求中的生命周期,请求结束时清空,防止内存泄漏
  • HeaderWriterFilter
    • 请求头过滤器
  • CorsFilter
    • 跨域过滤器
  • CsrfFilter
    • csrf过滤器
  • LogoutFilter
    • 登出过滤器
  • OAuth2AuthorizationRequestRedirectFilter
    • Oauth2请求鉴权重定向过滤器,需配合OAuth2.0的模块使用
  • Saml2WebSsoAuthenticationRequestFilter
    • Saml2单点认证过滤器 需配合Spring Security SAML模块使用
  • X509AuthenticationFilter
    • X.509证书认证过滤器
  • AbstractPreAuthenticatedProcessingFilter
    • 处理经过预先认证的身份验证请求的过滤器的基类
  • CasAuthenticationFilter
    • CAS 单点登录认证过滤器 。配合Spring Security CAS模块使用
  • OAuth2LoginAuthenticationFilter
    • OAuth2 登录认证过滤器
  • Saml2WebSsoAuthenticationFilter
    • SMAL 的 SSO 单点登录认证过滤器
  • UsernamePasswordAuthenticationFilter
    • 用户名密码认证过滤器
  • OpenIDAuthenticationFilter
    • OpenID认证过滤器
  • DefaultLoginPageGeneratingFilter
    • 默认登入页生成过滤器
  • DefaultLogoutPageGeneratingFilter
    • 默认登出页生成过滤器
  • ConcurrentSessionFilter
    • session管理,用于判断session是否过期
  • DigestAuthenticationFilter
    • 摘要认证过滤器
  • BearerTokenAuthenticationFilter
    • Bearer标准token认证过滤器
  • BasicAuthenticationFilter
    • Http Basic标准认证过滤器
  • RequestCacheAwareFilter
    • 请求缓存过滤器,主要作用是认证完成后恢复认证前的请求继续执行
  • SecurityContextHolderAwareRequestFilter
    • 对request包装的目的主要是实现servlet api的一些接口方法isUserInRole、getRemoteUser
  • JaasApiIntegrationFilter
    • Jaas认证过滤器
  • RememberMeAuthenticationFilter
    • RememberMe 认证过滤器
  • AnonymousAuthenticationFilter
    • 匿名认证过滤器
  • OAuth2AuthorizationCodeGrantFilter
    • OAuth2授权码过滤器
  • SessionManagementFilter
    • Session 管理器过滤器,内部维护了一个SessionAuthenticationStrategy 用于管理 Session
  • ExceptionTranslationFilter
    • 异常翻译过滤器
  • FilterSecurityInterceptor
    • 请求鉴权过滤器
  • SwitchUserFilter

    • 账户切换过滤器

      2、FilterSecurityInterceptor

      是一个方法级的权限过滤器,位于过滤器的最底部。

      public void invoke(FilterInvocation fi) throws IOException, ServletException {
         if ((fi.getRequest() != null)
                 && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                 && observeOncePerRequest) {
             // filter already applied to this request and user wants us to observe
             // once-per-request handling, so don't re-do security checking
             fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
         }
         else {
             // first time this request being called, so perform security checking
             if (fi.getRequest() != null && observeOncePerRequest) {
                 fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
             }
      
             InterceptorStatusToken token = super.beforeInvocation(fi);
      
             try {
                 fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
             }
             finally {
                 super.finallyInvocation(token);
             }
      
             super.afterInvocation(token, null);
         }
      }
      

      3、ExceptionTranslationFilter

  • 异常过滤器

  • 用来处理在认证授权过程中抛除的异常

      public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
              throws IOException, ServletException {
          HttpServletRequest request = (HttpServletRequest) req;
          HttpServletResponse response = (HttpServletResponse) res;
    
          try {
              chain.doFilter(request, response);
    
              logger.debug("Chain processed normally");
          }
          catch (IOException ex) {
              throw ex;
          }
          catch (Exception ex) {
              // Try to extract a SpringSecurityException from the stacktrace
              Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
              RuntimeException ase = (AuthenticationException) throwableAnalyzer
                      .getFirstThrowableOfType(AuthenticationException.class, causeChain);
    
              if (ase == null) {
                  ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                          AccessDeniedException.class, causeChain);
              }
    
              if (ase != null) {
                  if (response.isCommitted()) {
                      throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
                  }
                  handleSpringSecurityException(request, response, chain, ase);
              }
              else {
                  // Rethrow ServletExceptions and RuntimeExceptions as-is
                  if (ex instanceof ServletException) {
                      throw (ServletException) ex;
                  }
                  else if (ex instanceof RuntimeException) {
                      throw (RuntimeException) ex;
                  }
    
                  // Wrap other Exceptions. This shouldn't actually happen
                  // as we've already covered all the possibilities for doFilter
                  throw new RuntimeException(ex);
              }
          }
      }
    

    4、UsernamePasswordAuthenticationFilter

  • 对/login的post请求做拦截,校验表单中用户名和密码。

    public Authentication attemptAuthentication(HttpServletRequest request,
              HttpServletResponse response) throws AuthenticationException {
          if (postOnly && !request.getMethod().equals("POST")) {
              throw new AuthenticationServiceException(
                      "Authentication method not supported: " + request.getMethod());
          }
    
          String username = obtainUsername(request);
          String password = obtainPassword(request);
    
          if (username == null) {
              username = "";
          }
    
          if (password == null) {
              password = "";
          }
    
          username = username.trim();
    
          UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                  username, password);
    
          // Allow subclasses to set the "details" property
          setDetails(request, authRequest);
    
          return this.getAuthenticationManager().authenticate(authRequest);
      }
    

    5、调用原理

    1、调用过程

  • 1、所有的过滤器被添加在HttpSecurity类中

      public HttpSecurity addFilter(Filter filter) {
          Class<? extends Filter> filterClass = filter.getClass();
          if (!comparator.isRegistered(filterClass)) {
              throw new IllegalArgumentException(
                      "The Filter class "
                              + filterClass.getName()
                              + " does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead.");
          }
          this.filters.add(filter);
          return this;
      }
    
    • 总共14个过滤器
    • image.png
  • 2、并且这些过滤器被重新封装在DefaultSecurityFilterChain中提供给DelegatingFilterProxy进行调用

      @Override
      protected DefaultSecurityFilterChain performBuild() {
          filters.sort(comparator);
          return new DefaultSecurityFilterChain(requestMatcher, filters);
      }
    
    • 通过WebSecurityConfiguration配置DefaultSecurityFilterChain过滤链来调用performBuild()方法
      @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
      public Filter springSecurityFilterChain() throws Exception {
         boolean hasConfigurers = webSecurityConfigurers != null
                 && !webSecurityConfigurers.isEmpty();
         if (!hasConfigurers) {
             WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
                     .postProcess(new WebSecurityConfigurerAdapter() {
                     });
             webSecurity.apply(adapter);
         }
         return webSecurity.build();
      }
      
  • 3、通过DelegatingFilterProxy这个类开始调用过滤器链

    @Override
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
              throws ServletException, IOException {
    
          // Lazily initialize the delegate if necessary.
          Filter delegateToUse = this.delegate;
          if (delegateToUse == null) {
              synchronized (this.delegateMonitor) {
                  delegateToUse = this.delegate;
                  if (delegateToUse == null) {
                      WebApplicationContext wac = findWebApplicationContext();
                      if (wac == null) {
                          throw new IllegalStateException("No WebApplicationContext found: " +
                                  "no ContextLoaderListener or DispatcherServlet registered?");
                      }
                      delegateToUse = initDelegate(wac);
                  }
                  this.delegate = delegateToUse;
              }
          }
    
          // Let the delegate perform the actual doFilter operation.
          invokeDelegate(delegateToUse, request, response, filterChain);
      }
    
    • initDelegate找到Proxy
      protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
         String targetBeanName = getTargetBeanName();
         Assert.state(targetBeanName != null, "No target bean name set");
         Filter delegate = wac.getBean(targetBeanName, Filter.class);
         if (isTargetFilterLifecycle()) {
             delegate.init(getFilterConfig());
         }
         return delegate;
      }
      

      2、原理中@Autowired在方法上的使用案例

  • 通过@Autowired注解让spring在启动的时候就扫码到这个方法,并执行,

  • @Value(“#{@….}”)的语法去获取某个对象

    @Autowired(required = false)
      public void setFilterChainProxySecurityConfigurer(
              ObjectPostProcessor<Object> objectPostProcessor,
              @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
              throws Exception {
          ......
      }
    
  • autowiredWebSecurityConfigurersIgnoreParents方法

      @Bean
      public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents(
              ConfigurableListableBeanFactory beanFactory) {
          return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory);
      }
    
  • 调用类里的getWebSecurityConfigurers方法

    final class AutowiredWebSecurityConfigurersIgnoreParents {
    
      private final ConfigurableListableBeanFactory beanFactory;
    
      AutowiredWebSecurityConfigurersIgnoreParents(
              ConfigurableListableBeanFactory beanFactory) {
          Assert.notNull(beanFactory, "beanFactory cannot be null");
          this.beanFactory = beanFactory;
      }
    
      @SuppressWarnings({ "rawtypes", "unchecked" })
      public List<SecurityConfigurer<Filter, WebSecurity>> getWebSecurityConfigurers() {
          List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new ArrayList<>();
          Map<String, WebSecurityConfigurer> beansOfType = beanFactory
                  .getBeansOfType(WebSecurityConfigurer.class);
          for (Entry<String, WebSecurityConfigurer> entry : beansOfType.entrySet()) {
              webSecurityConfigurers.add(entry.getValue());
          }
          return webSecurityConfigurers;
      }
    }
    

    3、自己实现案例

  • 在一个配置类中写了这么一段代码

      @Autowired()
      public void autowiredTest(@Value("#{@auto.getString()}") String name){
          System.out.println("我被执行了"+name);
      }
    
      @Bean
      public static AutoTest auto(){
          return new AutoTest();
      }
    
  • 并且定义了一个final类 ```java package com.daijunyi.auto;

/**

  • @author djy
  • @createTime 2021/12/14 上午9:49
  • @description */ public final class AutoTest {

    public String getString(){

     return "hello world";
    

    } }


- 执行就会打印出:我被执行了hello world
<a name="aEdEs"></a>
## 6、两个重要的接口用数据库查用户名和密码
<a name="pPcX9"></a>
### 1、UserDetailService
当什么也没配置的时候,账号和密码是由Spring Security定义生成的。

- 实现自定义逻辑只需要,实现UserDetailService接口,并实现里面的loadUserByUsername(String username);就可以
```java
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • 返回值是UserDetails

image.png

  • org.springframework.security.core.userdetails.User 是UserDetails的一个实现类
  • 方法参数username

    • 是客户端表单传递过来的数据,默认情况下必须叫username

      2、PasswordEncoder接口

      public interface PasswordEncoder {
      //对原始密码进行编码。一般来说,
      //一个好的编码算法应用SHA-1或更大的哈希值与一个8字节或更大的随机生成的salt相结合
      String encode(CharSequence rawPassword);
      //验证从存储器获得的编码密码在编码后是否与提交的原始*密码匹配。
      //如果密码匹配,则返回true;如果*密码不匹配,则返回false。
      //存储的密码本身永远不会被解码
      boolean matches(CharSequence rawPassword, String encodedPassword);
      //如果为了更好的安全性,应再次对编码的密码进行编码,则返回true,
      //否则返回false。默认实现始终返回false。
      //@param encodedPassword编码密码以检查
      //@如果为了更好的安全性需要再次编码编码密码,则返回true,*否则返回false
      default boolean upgradeEncoding(String encodedPassword) {
         return false;
      }
      }
      
  • org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder

    • 使用BCrypt强哈希函数的PasswordEncoder的实现。客户端可以选择提供一个“版本”($2a、$2b、$2y)和一个“强度”(也称为BCrypt中的日志轮次)以及一个SecureRandom实例。强度参数越大,需要做的工作就越多*(指数级)以散列密码。默认值为10
      • 是对bcrypt强散列方法的具体实现,基于Hash算法实现的单向加密,可以通过strength控制加密强度,默认10
    • 是官方推荐密码解析器,平时用这个
  • 使用演示

    @Test
      public void bCryptTest(){
          BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
          String djy = bCryptPasswordEncoder.encode("djy");
          System.out.println("加密后数据:"+djy);
          boolean isOk = bCryptPasswordEncoder.matches("djy", djy);
          System.out.println("密码是否匹配:"+isOk);
      }
    
    • 演示结果 ```shell 加密后数据:$2a$10$ZteV1CibdooWCoKN39kgyOtRIRgnLxEwPf7GURYM5d3QFeoNzXxN6 密码是否匹配:true

Process finished with exit code 0

<a name="ymgv1"></a>
# 4、配置密码的三种方式
<a name="tuR7v"></a>
## 1、配置文件配置

- 配置文件配置账号密码
- 这种方式一般是用不到的。
```yaml
spring:
  security:
    user:
      password: 123456
      name: daijunyi

2、编写配置类

  • 配置BCryptPasswordEncoder加密方式 ```java @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean public PasswordEncoder passwordEncoder() {

      return new BCryptPasswordEncoder();
    

    }

    @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {

      auth.inMemoryAuthentication()
              .withUser("djy")
              .password(passwordEncoder().encode("123456"))
              .roles("admin");
    

    } }

<a name="c4pt1"></a>
## 3、从数据库读取
<a name="hhYy8"></a>
### 1、修改pom
```xml
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.daijunyi</groupId>
    <artifactId>security</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>security</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <!--        不从本地仓库获取-->
        <relativePath></relativePath>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <!--只在测试的时候用-->
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.27</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

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

    </dependencies>

    <build>
        <resources> <!-- 资源文件的打包和过滤替换配置 -->
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.yml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2、修改yaml

server:
  port: 8083
spring:
  datasource:
    url: jdbc:mysql://192.168.33.10:3306/asto
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

3、pojo类

  • PartyVo ```java @Data public class PartyVo {

    private Integer partyId;

    private String nickName;

    private String password; }

<a name="Ko5fV"></a>
### 4、mapper
<a name="stTtA"></a>
#### 1、mapper接口
```java
@Mapper
public interface PartyMapper {

    PartyVo getPartyByNickName(@Param("nickName") String nickName);

}

2、mapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.daijunyi.mapper.PartyMapper">

    <resultMap id="partyVoResultMap" type="com.daijunyi.pojo.vo.PartyVo">
        <id column="id" property="partyId"></id>
        <result column="nick_name" property="nickName"></result>
    </resultMap>

    <select id="getPartyByNickName" resultMap="partyVoResultMap">
        SELECT id,nick_name,password FROM party where del_flag = 0 AND nick_name = #{nickName}
    </select>

</mapper>

5、添加UserDetailsService的实现类

@Service
public class SecurityUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private PartyMapper partyMapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        PartyVo party = partyMapper.getPartyByNickName(username);
        if (ObjectUtils.isEmpty(party)){
            throw new UsernameNotFoundException("账号密码错误");
        }

        return new User(party.getNickName(),passwordEncoder.encode(party.getPassword()), AuthorityUtils.commaSeparatedStringToAuthorityList("admin,"));
    }


}

3、配置SecurityConfig

  • 配置加密方式 ```java package com.daijunyi.config;

import com.daijunyi.service.impl.SecurityUserDetailsServiceImpl; 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 djy
  • @createTime 2021/12/13 上午7:26
  • @description */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean public PasswordEncoder passwordEncoder() {

     return new BCryptPasswordEncoder();
    

    }

    @Autowired private UserDetailsService userDetailsService;

    @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {

     auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    

    } }

<a name="p5X2y"></a>
# 5、权限相关与其他配置
```java
@Override
    protected void configure(HttpSecurity http) throws Exception {
        //退出
        http.logout().logoutUrl("/logout").
                logoutSuccessUrl("/test/hello").permitAll();

        //配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");
        http.formLogin()   //自定义自己编写的登录页面
            .loginPage("/on.html")  //登录页面设置
            .loginProcessingUrl("/user/login")   //登录访问路径
            .defaultSuccessUrl("/success.html").permitAll()  //登录成功之后,跳转路径
                .failureUrl("/unauth.html")
            .and().authorizeRequests()
                .antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
                //当前登录用户,只有具有admins权限才可以访问这个路径
                //1 hasAuthority方法
               // .antMatchers("/test/index").hasAuthority("admins")
                //2 hasAnyAuthority方法
               // .antMatchers("/test/index").hasAnyAuthority("admins,manager")
                //3 hasRole方法   ROLE_sale
                .antMatchers("/test/index").hasRole("sale")

                .anyRequest().authenticated()
                .and().rememberMe().tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(60)//设置有效时长,单位秒
                .userDetailsService(userDetailsService);
               // .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
          // .and().csrf().disable();  //关闭csrf防护
    }

1、接口权限相关设置

  • hasAuthority(“admins”)
    • 表示有admins角色权限
  • hasAnyAuthority(“admins,manager”) 多个权限
    • 表示有admins和manager权限,可以设置多个
  • hasRole(“sale”)/hasAnyRole(“sale,sale2”)�

    • 设置拥有角色
    • 源码添加了ROLE_
      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 + "'");
         }
         return "hasRole('ROLE_" + role + "')";
      }
      

      2、用户权限设置

  • 通过AuthorityUtils.commaSeparatedStringToAuthorityList(“admin,ROLE_sale”)设置到UserDetail中

  • 注意点如果是设置的是角色,就需要加上ROLE_前缀

      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
          //调用usersMapper方法,根据用户名查询数据库
          QueryWrapper<Users> wrapper = new QueryWrapper();
          // where username=?
          wrapper.eq("username",username);
          Users users = usersMapper.selectOne(wrapper);
          //判断
          if(users == null) {//数据库没有用户名,认证失败
              throw  new UsernameNotFoundException("用户名不存在!");
          }
          List<GrantedAuthority> auths =
                  AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
          //从查询数据库返回users对象,得到用户名和密码,返回
          return new User(users.getUsername(),
                  new BCryptPasswordEncoder().encode(users.getPassword()),auths);
      }
    

    6、注解使用

    1、注解@Secured

  • 功能判断是否拥有某个角色,并且要加上前缀ROLE_

如果使用该注解需要先开启注解功能

  • @EnableGlobalMethodSecurity(securedEnabled = true) ```java @Configuration @EnableGlobalMethodSecurity(securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {

}


- 使用方法
   - 在controller请求中添加
```java
@RestController
@RequestMapping("/hello")
public class HelloController {

    @GetMapping("/security")
    @Secured({"ROLE_normal","ROLE_admins"})
    public String saySecurity(){
        return "hello security";
    }

}

2、注解@PreAuthorize

  • 可以将登陆用户的roles/permissions参数传到方法中

也需要开启注解功能

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

}
  • 使用 ```java @RestController @RequestMapping(“/hello”) public class HelloController {

    @GetMapping(“/security”) @PreAuthorize(“hasAnyAuthority(‘admins’)”) public String saySecurity(){

      return "hello security";
    

    }

}


<a name="xnfQr"></a>
## 3、@PostAuthorize
也需要开启注解功能
```java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

}
  • 在执行方法之后再进行权限验证,适合验证带有返回值的权限

    @RequestMapping("/testPostAuthorize")
    @ResponseBody
    @PostAuthorize("hasAnyAuthority('menu:system')")
    public String preAuthorize(){
      System.out.println("test--PostAuthorize");
      return "PostAuthorize";
    }
    

    4、@PostFilter

  • @PostFilter :权限验证之后对数据进行过滤留下用户名是admin1 的数据表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素

    @RequestMapping("getAll")
    @PreAuthorize("hasRole('ROLE_管理员')")
    @PostFilter("filterObject.username == 'admin1'")
    @ResponseBody
    public List<UserInfo> getAllUser(){
      ArrayList<UserInfo> list = new ArrayList<>();
      list.add(new UserInfo(1l,"admin1","6666"));
      list.add(new UserInfo(2l,"admin2","888"));
      return list;
    }
    

    5、@Prefilter

  • PreFilter:进入控制器之前对数据进行过滤

image.png

6、相关表达式

https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#el-access