SpringSecurity的概念、原理和简单的应用

一、SpringSecurity的介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的(可配置的)安全访问控制解决方案的安全框架(简单说是对访问权限进行控制 )。
它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IOC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
应用的安全性包括:会话管理、密码加密、用户认证(Authentication)和用户授(Authorization)四个部分。后两个功能是最常用的两个功能。
       用户认证(你是谁?):验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统 。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程
       用户授权(你来干嘛?):验证某个用户是否有权限执行某个操作在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限

二、SpringSecurity的实现原理

1.默认登录页及配置的总体介绍

  1. WebSecurityConfigurerAdapter是SpringSecurity核心配置类,它提供默认的配置 ,这个抽象类中,提供了一个方法formLogin(),内容如下:

    protected void configure(HttpSecurity http) throws Exception {
    	logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
    
    	http
    		.authorizeRequests()
    			.anyRequest().authenticated()
    			.and()
    		.formLogin().and()
    		.httpBasic();
    }
    

    SpringSecurity的概念、原理和简单的应用

  2. 点击formLogin()查看formLogin()源码,跳转到HttpSecurity类中,这个方法返回一个 FormLoginConfigurer类型,再点击FormLoginConfigurer进入继续来看看这个FormLoginConfigurer,在FormLoginConfigurer中有个initDefaultLoginFilter()方法

    	private void initDefaultLoginFilter(H http) {
    	DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
    			.getSharedObject(DefaultLoginPageGeneratingFilter.class);
    	if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {
    		loginPageGeneratingFilter.setFormLoginEnabled(true);
    		loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter());
    		loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter());
    		loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());
    		loginPageGeneratingFilter.setFailureUrl(getFailureUrl());
    		loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());
    	}
    }
    

    SpringSecurity的概念、原理和简单的应用
    SpringSecurity的概念、原理和简单的应用SpringSecurity的概念、原理和简单的应用SpringSecurity的概念、原理和简单的应用

  3. initDefaultLoginFilter()这个方法,初始化一个默认登录页的过滤器,可以看到第一句代码,默认的过滤器是DefaultLoginPageGeneratingFilter ,进入到这个过滤器中

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    		throws IOException, ServletException {
    	HttpServletRequest request = (HttpServletRequest) req;
    	HttpServletResponse response = (HttpServletResponse) res;
    
    	boolean loginError = isErrorPage(request);
    	boolean logoutSuccess = isLogoutSuccess(request);
    	if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
    		String loginPageHtml = generateLoginPageHtml(request, loginError,
    				logoutSuccess);
    		response.setContentType("text/html;charset=UTF-8");
    		response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
    		response.getWriter().write(loginPageHtml);
    
    		return;
    	}
    
    	chain.doFilter(request, response);
    }
    

    SpringSecurity的概念、原理和简单的应用SpringSecurity的概念、原理和简单的应用SpringSecurity的概念、原理和简单的应用

在描述中可以看到,如果没有配置login页,这个过滤器会被创建,然后看doFilter()方法,登录页面的配置是通过generateLoginPageHtml()方法创建的

  1. 再来看看这个generateLoginPageHtml()方法内容

    	private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
    		boolean logoutSuccess) {
    	String errorMsg = "Invalid credentials";
    
    	if (loginError) {
    		HttpSession session = request.getSession(false);
    
    		if (session != null) {
    			AuthenticationException ex = (AuthenticationException) session
    					.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    			errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";
    		}
    	}
    
    	StringBuilder sb = new StringBuilder();
    
    	sb.append("<!DOCTYPE html>\n"
    			+ "<html lang=\"en\">\n"
    			+ "  <head>\n"
    			+ "    <meta charset=\"utf-8\">\n"
    			+ "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n"
    			+ "    <meta name=\"description\" content=\"\">\n"
    			+ "    <meta name=\"author\" content=\"\">\n"
    			+ "    <title>Please sign in</title>\n"
    			+ "    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"
    			+ "    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
    			+ "  </head>\n"
    			+ "  <body>\n"
    			+ "     <div class=\"container\">\n");
    
    	String contextPath = request.getContextPath();
    	if (this.formLoginEnabled) {
    		sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n"
    				+ "        <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
    				+ createError(loginError, errorMsg)
    				+ createLogoutSuccess(logoutSuccess)
    				+ "        <p>\n"
    				+ "          <label for=\"username\" class=\"sr-only\">Username</label>\n"
    				+ "          <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
    				+ "        </p>\n"
    				+ "        <p>\n"
    				+ "          <label for=\"password\" class=\"sr-only\">Password</label>\n"
    				+ "          <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n"
    				+ "        </p>\n"
    				+ createRememberMe(this.rememberMeParameter)
    				+ renderHiddenInputs(request)
    				+ "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
    				+ "      </form>\n");
    	}
    
    	if (openIdEnabled) {
    		sb.append("      <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n"
    				+ "        <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n"
    				+ createError(loginError, errorMsg)
    				+ createLogoutSuccess(logoutSuccess)
    				+ "        <p>\n"
    				+ "          <label for=\"username\" class=\"sr-only\">Identity</label>\n"
    				+ "          <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
    				+ "        </p>\n"
    				+ createRememberMe(this.openIDrememberMeParameter)
    				+ renderHiddenInputs(request)
    				+ "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
    				+ "      </form>\n");
    	}
    
    	if (oauth2LoginEnabled) {
    		sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
    		sb.append(createError(loginError, errorMsg));
    		sb.append(createLogoutSuccess(logoutSuccess));
    		sb.append("<table class=\"table table-striped\">\n");
    		for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
    			sb.append(" <tr><td>");
    			String url = clientAuthenticationUrlToClientName.getKey();
    			sb.append("<a href=\"").append(contextPath).append(url).append("\">");
    			String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue());
    			sb.append(clientName);
    			sb.append("</a>");
    			sb.append("</td></tr>\n");
    		}
    		sb.append("</table>\n");
    	}
    
    	if (this.saml2LoginEnabled) {
    		sb.append("<h2 class=\"form-signin-heading\">Login with SAML 2.0</h2>");
    		sb.append(createError(loginError, errorMsg));
    		sb.append(createLogoutSuccess(logoutSuccess));
    		sb.append("<table class=\"table table-striped\">\n");
    		for (Map.Entry<String, String> relyingPartyUrlToName : saml2AuthenticationUrlToProviderName.entrySet()) {
    			sb.append(" <tr><td>");
    			String url = relyingPartyUrlToName.getKey();
    			sb.append("<a href=\"").append(contextPath).append(url).append("\">");
    			String partyName = HtmlUtils.htmlEscape(relyingPartyUrlToName.getValue());
    			sb.append(partyName);
    			sb.append("</a>");
    			sb.append("</td></tr>\n");
    		}
    		sb.append("</table>\n");
    	}
    	sb.append("</div>\n");
    	sb.append("</body></html>");
    
    	return sb.toString();
    }
    

综上,默认登录页及配置的总体介绍。

2.用户名和密码生成原理

  1. 在项目启动的日志中,可以发现有这样一条信息 :

    2021-01-21 20:46:40.263  INFO 6416 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 
    
    Using generated security password: 160fdae6-d8f0-4a6c-8f0f-67db850f1d8e
    
    2021-01-21 20:46:40.602  INFO 6416 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@d2291de, org.springf
    

    SpringSecurity的概念、原理和简单的应用

    可以看到,自动配置类是UserDetailsServiceAutoConfiguration,密码是 :160fdae6-d8f0-4a6c-8f0f-67db850f1d8e,现在知道了密码,那用户名是什么还不知道。

  2. 进入到 UserDetailsServiceAutoConfiguration去,在这个UserDetailsServiceAutoConfiguration 类的描述中可以知道,这个类是设置一些 Spring Security 相关默认的自动配置,把InMemoryUserDetailsManager 中得user 和 password 信息设置为默认得用户和密码,可以通过提供的AuthenticationManager、AuthenticationProvider 或者 UserDetailsService 的 bean 来覆盖默认的自动配置信息

    	@Bean
    	@ConditionalOnMissingBean(
    			type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
    	@Lazy
    	public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
    			ObjectProvider<PasswordEncoder> passwordEncoder) {
    		SecurityProperties.User user = properties.getUser();
    		List<String> roles = user.getRoles();
    		return new InMemoryUserDetailsManager(
    				User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
    						.roles(StringUtils.toStringArray(roles)).build());
    	}
    
    	private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
    		String password = user.getPassword();
    		if (user.isPasswordGenerated()) {
    			logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
    		}
    		if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
    			return password;
    		}
    		return NOOP_PASSWORD_PREFIX + password;
    	}
    
  3. 可以看到,日志输出的密码是通过inMemoryUserDetailsManager()方法获取,返回一个新的带有UserDetials信息参数构造的InMemoryUSerDetailsManager对象 ,第一个参数为:User.withUsername(user.getName()),
    其中user 对象是上面SecurityProperties.User类型 的,通过SecurityProperties 对象中获取的 ,看下SecurityProperties类 :

    @ConfigurationProperties(prefix = "spring.security")
    public class SecurityProperties {
    	...
    }
    public static class User {
    
    		/**
    		 * Default user name.
    		 */
    		private String name = "user";
    
    		/**
    		 * Password for the default user name.
    		 */
    		private String password = UUID.randomUUID().toString();
    
    		/**
    		 * Granted roles for the default user name.
    		 */
    		private List<String> roles = new ArrayList<>();
    
    		private boolean passwordGenerated = true;
    
    		public String getName() {
    			return this.name;
    		}
    		...
    	}
    

SpringSecurity的概念、原理和简单的应用SpringSecurity的概念、原理和简单的应用

> 通过配置文件中的,前缀为spring.security 的配置可以改变默认配置信息,再看看SecurityProperties 的getUser()方法 ,通过一步步的跟踪,发现默认的用户名是user 。

3.框架核心过滤器

        想要对WEB资源进行保护,最好的办法就是Filter,想要对方法进行保护,最好的办法就是AOP,SpringSecurity在我们进行用户认证和授权的时候,会通过各种各样的拦截器来控制权限的访问,从而实现安全。SpringSecurity常见的过滤器有:

Filter 含义
WebAsyncManagerIntegrationFilter 异步 , 提供了对securityContext和WebAsyncManager的集 成
SecurityContextPersistenceFilter 同步 , 从配置的SecurityContextRepository而不是request 中获取信息存到SecurityContextHolder,并且当请求结束清 理contextHolder时将值存回repository中(默认使用 HttpSessionSecurityContextRepository).在该过滤器中每一 个请求仅执行一次,该filter需在任何认证处理机制其作用之 前执行。认证处理机制如basic,cas等期望在执行时从 SecurityContextHolder中获取SecurityContext
HeaderWriterFilter 是一个向HttpServletResponse写入http请求头的约定
CsrfFilter 通过使用同步token模式来进行csrf防护
LogoutFilter 记录用户的退出
RequestCacheAwareFilter 用于用户登录成功后,重新恢复因为登录被打断的请求 , 请 求信息被保存到cache中
SecurityContextHolderAwareRequestFilter 包装请求对象request
AnonymousAuthenticationFilter 是在UsernamePasswordAuthenticationFilter、 BasicAuthenticationFilter、 RememberMeAuthenticationFilter这些过滤器后面的,所 以如果这三个过滤器都没有认证成功,则为当前的 SecurityContext中添加一个经过匿名认证的token,但是通 过servlet的getRemoteUser等方法是获取不到登录账号的。 因为SecurityContextHolderAwareRequestFilter过滤器在 AnonymousAuthenticationFilter前面
SessionManagementFilter 管理session
ExceptionTranslationFilter 处理过滤器链抛出的所有AccessDeniedException和 AuthenticationException异常
FilterSecurityInterceptor 通过实现了filter来增加http资源的安全性。这个安全拦截器 需要FilterInvocationSecurityMedataSource
UsernamePasswordAuthenticationFilter 登陆用户密码验证过滤器 ,基于用户名和密码的认证逻辑
BasicAuthenticationFilter 处理一个http请求的basic认证头,将结果放入 SecurityContextHolder
DefaultLoginPageGeneratingFilter 当一个用户没有配置login页面时使用。仅当跳转到login页面 时用到

4.核心组件

1.Authentication

  • Authentication 是一个接口,用来表示用户认证信息的。

  • 在用户登录认证之前相关信息会封装为一个Authentication 具体实现类的对象 ,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder所持有的 SecurityContext 中,供后续的程序进 行调用,如访问权限的鉴定等。

  • Authentication 对象不需要我们自己去创建,在与系统交互的过程中,Spring Security 会自动为我们创建相应的 Authentication 对象 ,然后赋值给当前的 SecurityContext ,但是往往我们需要在程序中获取当前用户的相关信息,比如最常见的是获取当前登录用户的用户名。在程序的任何地方,通过如下方式我们可以获取到当前用户的用户名。

    public String getCurrentUsername() {
    	//getAuthorities:权限信息
        //getCredentials:认证信息(证书信息)
        //getPrincipal:用户信息
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        //通过instanceof 判断是否获取了自定义用户实体对象
        if (principal instanceof User) {
            return ((User) principal).getName();
        }
        return null;
    }
    

此外,调用 SecurityContextHolder.getContext() 获取 SecurityContext 时,如果对应的SecurityContext 不存在,则 Spring Security 将为我们建立一个空的 SecurityContext 并进行返回

2.SecurityContextHolder

  • SecurityContextHolder 是用来保存 SecurityContext的
  • SecurityContext 中含有当前正在访问系统的用户的详细信息
  • 默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存SecurityContext ,这也就意味着在处于同一线程中的方法中我们可以从 ThreadLocal 中获取到当前的 SecurityContext,因为线程池的原因,如果我们每次在请求完成后都将 ThreadLocal 进行清除的话,那么我们把SecurityContext 存放在 ThreadLocal 中还是比较安全的
  • 这些工作 Spring Security 已经自动为我们做了,即在每一次 request 结束后都将清除当前线程的ThreadLocal
  • SecurityContextHolder 中定义了一系列的静态方法,而这些静态方法内部逻辑基本上都是通过SecurityContextHolder 持有的SecurityContextHolderStrategy 来实现的,如 getContext()、setContext()、clearContext()等
  • 默认使用的 strategy 就是基于 ThreadLocal 的ThreadLocalSecurityContextHolderStrategy
  • Spring Security 还提供了两种类型的 strategy 实现,GlobalSecurityContextHolderStrategy 和InheritableThreadLocalSecurityContextHolderStrategy ,前者表示全局使用同一个 SecurityContext,如C/S 结构的客户端;后者使用InheritableThreadLocal 来存放 SecurityContext,即子线程可以使用父线程中
    存放的变量
  • 一般而言,我们使用默认的 strategy 就可以了,但是如果要改变默认的 strategy,Spring Security 为我们提供了两种方法,这两种方式都是通过改变 strategyName 来实现的 。SecurityContextHolder 中为三种不同类型的 strategy 分别命名为 MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL 和 MODE_GLOBAL ,第一种方式是通过 SecurityContextHolder 的静态方法 setStrategyName() 来指定需要使用的 strategy;第二种方式是通过系统属性进行指定,其中属性名默认为 “spring.security.strategy”,属性值为对应 strategy 的名称。

3.AuthenticationManager 和AuthenticationProvider

  • AuthenticationManager 是一个用来处理认证(Authentication)请求的接口认证是由 AuthenticationManager 来管理的,但是真正进行认证的是AuthenticationManager 中定义的AuthenticationProvider。

  • AuthenticationManager 中可以定义有多个 AuthenticationProvider在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。

    package com.kejizhentan.security;
    
    import com.kejizhentan.entity.User;
    import com.kejizhentan.service.IUserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    
    /**
    * @Description:    用户信息动态验证的实现类(这个类相当于用户登录的业务控制层controller类)
    * @Author:         kejizhentan
    * @CreateDate:     2021/1/31 21:09
    * @UpdateRemark:   无
    * @Version:        1.0
    */
    
    public class MyAuthenticationProvider implements AuthenticationProvider {
    	/**
         * @author kejizhentan
         * @params  Authentication
         * @return  Authentication
         * @date 2021/1/31 21:10
         * @Description 用户信息判断的业务逻辑方法
         */
        @Override
        public Authentication authenticate(Authentication auth) throws AuthenticationException {
    	if(判断认证是否成功){
            //登录成功业务处理
            //参数1:代表用户实体对象
            //参数2:代表的安全证书信息
            //参数3:角色信息(集合)
            return  new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
            }
            //如果有数据返回则代表用户信息验证成功,如果返回null则表示用户名或者密码错误
            //安全框架中认证失败的话都是通过抛异常来解决的,而不是用return来提示的
            throw new BadCredentialsException("authError");
        }
    
        /**
         * @author kejizhentan
         * @params 无
         * @return  boolean类型,返回true表示支持动态验证,false表示不支持动态验证
         * @Description 是否支持动态信息验证的设置类
        */
    
        @Override
        public boolean supports(Class<?> aClass) {
            return true;
        }
    }
    
    
  • 在 Spring Security 中,AuthenticationManager 的默认实现是ProviderManager,而且它不直接自己处理认证请求,而是委托给其所配置的 AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果,如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个ProviderNotFoundException,校验认证请求最常用的方法是根据请求的用户名加载对应的 UserDetails,然后比对 UserDetails 的密码与认证请求的密码是否一致,一致则表示认证通过Spring Security 内部的 DaoAuthenticationProvider 就是使用的这种方式。其内部使用 UserDetailsService来负责加载 UserDetails ,在认证成功以后会使用加载的 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext 中默认情况下,在认证成功后 ProviderManager 将清除返回的 Authentication 中的凭证信息,如密码。

  • 如果你在无状态的应用中将返回的 Authentication 信息缓存起来了,那么以后你再利用缓存的信息去认证将会失败,因为它已经不存在密码这样的凭证信息了。所以在使用缓存的时候你应该考虑到这个问题:一种解决办法是设置 ProviderManager 的eraseCredentialsAfterAuthentication 属性为 false,或者想办法在缓存时将凭证信息一起缓存。

4.UserDetailsService

  • 通过 Authentication.getPrincipal() 的返回类型是 Object,但很多情况下其返回的其实是一个 UserDetails 的实例。

  • UserDetails 是 Spring Security 中一个核心的接口 ,其中定义了一些可以获取用户名、密码、权限等与认证相关的信息的方法

  • Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,我们如果要使用 UserDetails 时也可以直接使用该类
    在 Spring Security 内部很多地方需要使用用户信息的时候基本上都是使用的UserDetails,比如在登录认证的时候。

  • 登录认证的时候 Spring Security 会通过 UserDetailsService 的loadUserByUsername() 方法获取对应的UserDetails 进行认证,认证通过后会将该 UserDetails 赋给认证通过的 Authentication 的 principal,然后再把该 Authentication 存入到 SecurityContext 中,之后如果需要使用用户信息的时候就是通过 SecurityContextHolder 获取存放在 SecurityContext 中的Authentication 的 principal。通常我们需要在应用中获取当前用户的其它信息,如 Email、电话等。这时存放在 Authentication 的principal 中只包含有认证相关信息的 UserDetails 对象可能就不能满足我们的要求了。这时我们可以实现自己的 UserDetails,在该实现类中我们可以定义一些获取用户其它信息的方法,这样将来我们就可以直接从当前 SecurityContext 的 Authentication 的 principal 中获取这些信息了。

  • UserDetailsService 也是一个接口,我们也需要实现自己UserDetailsService 来加载我们自定义的UserDetails 信息。然后把它指定的AuthenticationProvider 即可。

  • 另外 Spring Security 还为我们提供了 UserDetailsService 另外一个实现,InMemoryDaoImplInMemoryDaoImpl 主要是测试用的,其只是简单的将用户信息保存在内存中。

    package com.kejizhentan.entity;
    
    import lombok.Data;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    import java.util.Collection;
    import java.util.Date;
    import java.util.List;
    
    /**
    * @Description:    用户的实体类
    * @Author:         kejizhentan
    * @CreateDate:     2021/2/3 20:28
    * @UpdateUser:     kejizhentan
    * @UpdateDate:     2021/2/3 20:28
    * @UpdateRemark:   无
    * @Version:        1.0
    */
    /**
     * @Data注解的作用
     *  添加注解 @Data,即可省去手写getter, setter, toString的麻烦
     *  如果只想给某个属性加上get方法,则直接在属性的上面加上@getter和@Setter注解即可
    */
    
    @Data
    public class User implements UserDetails {
        /**
         *@Getter
         *@Setter
        */
        private Long userId;//用户ID
        private String username;//用户姓名
        private String email;//邮箱
        private String phone;//手机
        private String password;//密码
        private int status;//用户状态 0-正常 1-封禁
        private Date createTime;//创建时间
        private Date lastLoginTime;//上次登录时间
        private Date lastUpdateTime;//上次更新记录时间
        private String avatar;//头像
        public List<GrantedAuthority> authorities;//给用户授权
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    
    package com.kejizhentan.utils;
    
    import com.kejizhentan.entity.User;
    import org.springframework.security.core.context.SecurityContextHolder;
    
    /**
     * @Author: kejizhentan
     * @Date: 2019/11/8 11:24
     * @Description: 用户信息工具类
     */
    public class UserInfoUtil {
    
    
        /**
        * @Description: 获取用户主键ID
        * @Author: kejizhentan
        * @Date: 2019/11/8 11:25
        * @Param: []
        * @Return: java.lang.Long
        * @Exception:
        */
        public static Long  getUserId(){
           return getUser().getUserId();
        }
    
        /**
        * @Description: 自定义方法获取用户对象信息
        * @Author: kejizhentan
        * @Date: 2019/11/8 11:27
        * @Param: []
        * @Return: com.xdl.entity.User
        * @Exception:
        */
        public static User getUser(){
            //getAuthorities:权限信息
            //getCredentials:认证信息(证书信息)
            //getPrincipal:用户信息
            Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            if(principal instanceof User){
                return (User) principal;
            }
            return null;
        }
    
    }
    
    

5.GrantedAuthority

  • Authentication 的 getAuthorities() 可以返回当前 Authentication 对象拥有的权限,即当前用户拥有的权限。其返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种权限。GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后赋予给 UserDetails,GrantedAuthority 中只定义了一个 getAuthority() 方法,该方法返回一个字符串,表示对应权限的字符串表示,如果对应权限不能用字符串表示,则应当返回 null。
  • Spring Security 针对 GrantedAuthority 有一个简单实现SimpleGrantedAuthority。该类只是简单的接收一个表示权限的字符串。Spring Security 内部的所有 AuthenticationProvider 都是使用SimpleGrantedAuthority 来封装 Authentication 对象。

三、认证过程梳理

  1. 用户使用用户名和密码进行登录
  2. Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken
  3. 将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证
  4. AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象
  5. 通过调用 SecurityContextHolder.getContext().setAuthentication(…) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext
  6. 在认证成功后,用户就可以继续操作去访问其它受保护的资源了,但是在访问的时候将会使用保存在 SecurityContext 中的 Authentication 对象进行相关的权限鉴定,如不存在对应的访问权限,则会返回 403 错误码

四、SpringBoot整合SpringSecurity实现登录功能

  • 新建Maven工程,并导入以下包:

     <dependencies>
            <!--thymelea模板的包-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
            <!--springboot前端的基础包-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--导入安框架springsecurity的依赖包-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <!--导入mysql包-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.12</version>
            </dependency>
            <!--导入mybatis包-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.2</version>
            </dependency>
            <!--导入jdbc包-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
            <!--导入对象工具包-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
            <!--jpa包-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
     </dependencies>
    
  • 准备数据库表结构

    create table user (
    	userId int (11),
    	username varchar (96),
    	email varchar (96),
    	phone varchar (45),
    	password varchar (300),
    	status int (2),
    	createTime datetime ,
    	lastLoginTime datetime ,
    	lastUpdateTime datetime ,
    	avatar varchar (765),
    	PRIMARY KEY (userId)
    ); 
    
  • 新建实体对象类(实现UserDetails )

    package com.kejizhentan.entity;
    
    import lombok.Data;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    import java.util.Collection;
    import java.util.Date;
    import java.util.List;
    
    /**
    * @Description:    用户的实体类
    * @Author:         kejizhentan
    * @CreateDate:     2021/2/3 20:28
    * @UpdateUser:     kejizhentan
    * @UpdateDate:     2021/2/3 20:28
    * @UpdateRemark:   无
    * @Version:        1.0
    */
    /**
     * @Data注解的作用
     *  添加注解 @Data,即可省去手写getter, setter, toString的麻烦
     *  如果只想给某个属性加上get方法,则直接在属性的上面加上@getter和@Setter注解即可
    */
    
    @Data
    public class User implements UserDetails {
        /**
         *@Getter
         *@Setter
        */
        private Long userId;//用户ID
        private String username;//用户姓名
        private String email;//邮箱
        private String phone;//手机
        private String password;//密码
        private int status;//用户状态 0-正常 1-封禁
        private Date createTime;//创建时间
        private Date lastLoginTime;//上次登录时间
        private Date lastUpdateTime;//上次更新记录时间
        private String avatar;//头像
        public List<GrantedAuthority> authorities;//给用户授权
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    
  • 配置application.xml文件

    #设置端口号
    server.port=9999
    
    #配置数据源
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/housedemo
    spring.datasource.username=root
    spring.datasource.password=123456
    
    #指定mybatis的数据文件xml目录
    mybatis.mapper-locations=classpath:mapper/*.xml
    #配置mybatis日志文件框架输出信息,打印到控制台
    mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    
  • 增加前端登录和首页面
    login.html:

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录</title></head>
    <body>
    <form th:action="@{/login}" method="post"><input type="text" id="username" name="username" placeholder="手机号"/> <br/>
        <input type="password" id="password" name="password" placeholder="密码"/> <br/>
        <p th:if="${param.authError}" style="color: red">用户名或者密码错误</p> <br/>
        <button type="submit">登录</button>
    </form>
    </body>
    </html>
    

    index.html:

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首页面</title></head>
    <body><h3>登录成功</h3> <br/
    <form th:action="@{/user/logout}" method="post" id="logoutForm">
        <button type="submit" form="logoutForm">注销</button>
    </form>
    </body>
    </html>
    
  • 增加接口页面跳转控制器

    @Controller
    public class LoginController {
        @Autowired
        private IUserService userService;
    
        @GetMapping("/user/toLogin")
        public String toLogin() {
            return "login";
        }
    
        @PostMapping("/user/logout")
        public String logout() {
            return "login";
        }
    }
    
  • 增加权限控制等类

    package com.kejizhentan.security;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Configurable;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationProvider;
    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.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    
    /**
    * @Description:   springSecurity的配置类
    * @Author:         kejizhentan
    * @CreateDate:     2021/1/10 21:48
    * @UpdateUser:     kejizhentan
    * @UpdateDate:     2021/1/10 21:48
    * @UpdateRemark:   无
    * @Version:        1.0
    */
    @Configuration
    @EnableWebSecurity
    public class MySecurityConfig extends WebSecurityConfigurerAdapter {
        /*
         * @author kejizhentan
         * @params   http请求
         * @return  无
         * @exception 无
         * @date 2021/1/26 20:21
        */
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()//默认所有的请求都是认证的
                   /**
                    *   以上两个类注解的作用
                    *       @Configuration
                    *       用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,
                    *       这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,
                    *       并用于构建bean定义,初始化Spring容器。
                    *       @EnableWebSecurity注解有两个作用:
                    *       1: 加载了WebSecurityConfiguration配置类, 配置安全认证策略。
                    *       2: 加载了AuthenticationConfiguration, 配置了认证信息。
                    *
                    * antMatchers:匹配接口地址http是否和参数一致
                    *   参数值:支持表达式(参数不是值,类似于占位符)
                    *       0:表示接下来的接口地址只能是数值(例如:http://www.kejizhentan.com/Hello/1或者http://www.kejizhentan.com/Hello?id=1)
                    *       *:表示接下来的接口地址后面只能有一级(例如:http://www.kejizhentan.com/Hello)
                    *       **:表示接下来的接口地址后面可以有多级(例如:http://www.kejizhentan.com/Hello/Hello/Hello/...)
                    *   permitAll():允许所有的角色访问(不拦截)
                    *   hasRole:当前http请求只允许参数是ADMIN的角色访问(该角色不拦截)
                    *   hasAnyRole("角色1","角色2","..."):当前http请求允许角色1、角色2.。。等多个角色访问(配置的角色不拦截)
                    * anyRequest():拦截所有请求
                    *   authenticated():判断http请求是否授权(角色)
                    * and:一个业务的结束
                    * .anyRequest().authenticated():如果注视掉此行代码,则说明/地址不拦截,即http://www.kejizhentan.com地址不拦截
                    *
                    *
                    *
                   */
    
                    .antMatchers("/user/toUserLogin").permitAll()
                    //.antMatchers("/user/toHello").hasRole("ADMIN")
                    //.antMatchers("/user/toHello").hasAnyRole("角色1","角色2","...")
                    //.anyRequest().authenticated()
                    .and()
                    .formLogin()
                    //自定义异常信息处理(在当前页面提示错误信息)
                    .failureHandler(authFailureHandler())
                    .and()
                    //退出功能
                    .logout()
                    //退出成功后跳转的页面地址
                    .logoutSuccessUrl("/user/toUserLogin")
                    .and()
                    .httpBasic();
                    /**
                     *
                     * 1.什么是 CSRF
                     *     CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
                     *     跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。
                     *     客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了 cookie 进行记录客户端身份。在 cookie 中会存放 session id 用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个 session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情
                     *2.Spring Security 中 CSRF
                     *     从 Spring Security4 开始 CSRF 防护默认开启。默认会拦截请求。进行 CSRF 处理。CSRF 为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf 值为 token(token 在服务端产生)的内容,如果 token 和服务端的 token 匹配成功,则正常访问。
                     * 网站伪造技术(防止恶意攻击的),开启网站伪造的话点击退出还是会跳转到springsecurity自定义的退出界面,然后再跳转到自己指定的页面
                     * 关闭网站伪造之后点击退出按钮就直接跳转到指定的登录界面了
                     */
                    http.csrf().disable();
                    http.headers().frameOptions().sameOrigin();
        }
        /**
         * @author kejizhentan
         * @params  无
         * @return  自定义异常处理类
         * @date 2021/1/31 20:54
         * @Description 自定义异常处理方法
        */
    
        public MyAuthenticationFailureHandler authFailureHandler() {
            return new MyAuthenticationFailureHandler();
        }
    
    
        /**
         * @author kejizhentan
         * @params
         * @return
         * @exception
         * @date 2021/1/27 21:23
         * @Description  自定义方法设置用户名和密码
         * @Remark:
         *      springsecurity设置账号和密码的时候一定要设置角色,否则会报:java.lang.IllegalArgumentException: Cannot pass a null GrantedAuthority collection
         *      解决方式:设置角色,角色名称可以是任意的非空字符串
         *
        */
    
        @Autowired
        public void configureConfig(AuthenticationManagerBuilder auth) throws Exception {
            /*设置静态的登录用户名和密码的方式
            auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("admin")
                    .password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN");*/
            //动态验证用户信息
            auth.authenticationProvider(authProvider());
    
        }
        /**
         * @author kejizhentan
         * @params  无
         * @date 2021/1/31 21:01
         * @Description  动态验证用户信息的方法
        */
        @Bean
        public MyAuthenticationProvider authProvider() {
            //验证业务信息
            return new MyAuthenticationProvider();
        }
    }
    
    package com.kejizhentan.security;
    
    import com.kejizhentan.entity.User;
    import com.kejizhentan.service.IUserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    
    /**
    * @Description:    用户信息动态验证的实现类(这个类相当于用户登录的业务控制层controller类)
    * @Author:         kejizhentan
    * @CreateDate:     2021/1/31 21:09
    * @UpdateRemark:   无
    * @Version:        1.0
    */
    
    public class MyAuthenticationProvider implements AuthenticationProvider {
        //非对称算法的工具类
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        @Autowired
        IUserService userService;
        /**
         * @author kejizhentan
         * @params  Authentication
         * @return  Authentication
         * @date 2021/1/31 21:10
         * @Description 用户信息判断的业务逻辑方法
         */
        @Override
        public Authentication authenticate(Authentication auth) throws AuthenticationException {
            //获取用户输入的用户名
            String inputname = auth.getName();
            //获取用户输入的密码
            String password = (String) auth.getCredentials();
            // 调用service-->dao-->用户名和密码当做参数传给sql中
            //根据用户输入的用户名查询用户信息,如果有数据返回,则代表用户信息是正确的
            User user = userService.queryUserByUserName(inputname);
            if(user == null){
                //安全(权限)框架的错误处理方式都是通过抛异常来解决的
                throw new BadCredentialsException("authError");
            }
            //参数1:代表用户输入的明文密码
            //参数2:代表数据库中的密文密码(根据用户对象查询)
            if(encoder.matches(password,user.getPassword())){
                //登录成功业务处理
                //参数1:代表用户实体对象
                //参数2:代表的安全证书信息
                //参数3:角色信息(集合)
                return  new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
            }
            //如果有数据返回则代表用户信息验证成功,如果返回null则表示用户名或者密码错误
            throw new BadCredentialsException("authError");
        }
    
        /**
         * @author kejizhentan
         * @params 无
         * @return  boolean类型,返回true表示支持动态验证,false表示不支持动态验证
         * @Description 是否支持动态信息验证的设置类
        */
    
        @Override
        public boolean supports(Class<?> aClass) {
            return true;
        }
    }
    
    
    package com.kejizhentan.security;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
    * @Description:    自定义错误页面的跳转信息类
    * @Author:         kejizhentan
    * @CreateDate:     2021/1/31 20:52
    * @Version:        1.0
    */
    
    public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            //自定义默认错误页面的跳转地址
            super.setDefaultFailureUrl("/user/toUserLogin?"+exception.getMessage());
            super.onAuthenticationFailure(request, response, exception);
        }
    }
    
  • 增加接口和mybatis信息

    package com.kejizhentan.service;
    
    import com.kejizhentan.entity.User;
    
    /**
    * @Description:    用户登录的service接口
    * @Author:         kejizhentan
    * @CreateDate:     2021/2/3 20:48
    * @UpdateUser:     kejizhentan
    * @UpdateDate:     2021/2/3 20:48
    * @UpdateRemark:   特殊说明
    * @Version:        1.0
    */
    
    public interface IUserService {
        /**
         * @author kejizhentan
         * @params 用户名
         * @return  返回用户对象
         * @exception 无
         * @date 2021/2/3 20:53
         * @Description 根据用户名查询用户对象
        */
        User queryUserByUserName(String username);
    }
    
    package com.kejizhentan.service.impl;
    
    import com.kejizhentan.dao.UserDAO;
    import com.kejizhentan.entity.User;
    import com.kejizhentan.service.IUserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
    * @Description:    用户登录Service的实现类
    * @Author:         kejizhentan
    * @CreateDate:     2021/2/3 20:58
    * @UpdateUser:     kejizhentan
    * @UpdateDate:     2021/2/3 20:58
    * @UpdateRemark:   特殊说明
    * @Version:        1.0
    */
    @Service
    public class UserServiceImpl implements IUserService {
        @Autowired
        UserDAO userDAO;
        @Override
        public User queryUserByUserName(String username) {
            return userDAO.queryUserByUserName(username);
        }
    }
    
    
    package com.kejizhentan.dao;
    
    import com.kejizhentan.entity.User;
    import org.apache.ibatis.annotations.Mapper;
    
    /**
    * @Description:    用户登录的持久化层DAO
    * @Author:         kejizhentan
    * @CreateDate:     2021/2/3 21:06
    * @UpdateUser:     kejizhentan
    * @UpdateDate:     2021/2/3 21:06
    * @UpdateRemark:   特殊说明
    * @Version:        1.0
    */
    public interface UserDAO {
        /**
         * @author kejizhentan
         * @params 用户名
         * @return  返回用户对象
         * @exception 无
         * @date 2021/2/3 20:53
         * @Description 根据用户名查询用户对象
         */
        User queryUserByUserName(String username);
    }
    
    <?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.kejizhentan.dao.UserDAO">
    
        <!--根据用户名查询用户信息-->
        <select id="queryUserByUserName" parameterType="java.lang.String" resultType="com.kejizhentan.entity.User">
            select * from user u where u.username = #{username}
        </select>
    </mapper>
    

注意:
     1.注意实体对象中GrantedAuthority类型List集合的封装
     2.注意加密算法BCryptPasswordEncoder的使用

上一篇:SpringSecurity 退出登录/修改密码/重置密码 使JWT的token失效的解决方案


下一篇:springsecurity-用户注销