Spring Security —09—密码加密

9.1 加密意义

2011年12月21 日,有人在网络上公开了一个包含600万个 CSDN 用户资料的数据库,数据全部为明文储存,包含用户名、密码以及注册邮箱。事件发生后,CSDN 在微博、官方网站等渠道发出了声明,解释说此数据库系2009 年备份所用,因不明原因泄漏,已经向警方报案,后又在官网发出了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入到这次事件中。整个事件中最触目惊心的莫过于 CSDN 把用户密码明文存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄漏就会造成很大的安全隐患。由于有了这么多前车之鉴,我们现在做系统时,密码都要加密处理。

在前面的案例中,凡是涉及密码的地方,我们都采用明文存储,在实际项目中这肯定是不可取的,因为这会带来极高的安全风险。在企业级应用中,密码不仅需要加密,还需要加盐,最大程度地保证密码安全。

9.2 常见方案

9.2.1 Hash 算法

最早我们使用类似 SHA-256 、SHA-512 、MD5等这样的单向 Hash 算法。用户注册成功后,保存在数据库中不再是用户的明文密码,而是经过 SHA-256 加密计算的一个字符串,当用户进行登录时,用户输入的明文密码用 SHA-256 进行加密,加密完成之后,再和存储在数据库中的密码进行比对,进而确定用户登录信息是否有效。如果系统遭遇攻击,最多也只是存储在数据库中的密文被泄漏(相同密码加密多次后结果均一致)。

这样就绝对安全了吗?由于彩虹表这种攻击方式的存在以及随着计算机硬件的发展,每秒执行数十亿次HASH计算己经变得轻轻松松,这意味着即使给密码加密加盐也不再安全。

9.2.2 单向自适应函数

在Spring Security 中,我们现在是用一种自适应单向函数 (Adaptive One-way Functions)来处理密码问题,这种自适应单向函数在进行密码匹配时,会有意占用大量系统资源(例如CPU、内存等),这样可以增加恶意用户攻击系统的难度。在Spring Securiy 中,开发者可以通过 bcrypt、PBKDF2、sCrypt 以及 argon2 来体验这种自适应单向函数加密。由于自适应单向函数有意占用大量系统资源,因此每个登录认证请求都会大大降低应用程序的性能,但是 Spring Secuity 不会采取任何措施来提高密码验证速度,因为它正是通过这种方式来增强系统的安全性(每次密码加密后结果均不一致)。

9.2.2.1 BCryptPasswordEncoder(首推+默认)

BCryptPasswordEncoder 使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt算法故意降低运行速度,以增强密码破解的难度。同时 BCryptPasswordEncoder 为自己带盐,开发者不需要额外维护一个“盐” 字段,使用 BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,即使相同的明文每次生成的加密字符串都不相同。

9.2.2.2 Argon2PasswordEncoder

Argon2PasswordEncoder 使用 Argon2 算法对密码进行加密,Argon2 曾在 Password Hashing Competition 竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题,Argon2也是故意降低运算速度,同时需要大量内存,以确保系统的安全性。

9.2.2.3 Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder 使用 PBKDF2 算法对密码进行加密,和前面几种类似,PBKDF2算法也是一种故意降低运算速度的算法,当需要 FIPS (Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2 算法是一个很好的选择。

9.2.2.4 SCryptPasswordEncoder

SCryptPasswordEncoder 使用scrypt 算法对密码进行加密,和前面的几种类似,scrypt 也是一种故意降低运算速度的算法,而且需要大量内存。

9.3密码验证流程

security的认证流程走到AbstractUserDetailsAuthenticationProvider的authenticate方法

@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

当通过retrieveUser获得到数据库中的用户信息UserDetails往下继续执行
在这里插入图片描述
进入check方法,做了一些账户状态的一些检查
在这里插入图片描述
继续执行additionalAuthenticationChecks,然后进入additionalAuthenticationChecks方法
DaoAuthenticationProvider.java

@Override
	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			this.logger.debug("Failed to authenticate since no credentials provided");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			this.logger.debug("Failed to authenticate since password does not match stored value");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
  • Authentication: 用户输入的信息
  • UserDetails: 数据库中查到的信息

通过passwordEncoder.matches对输入的密码和数据库查到的密码进行比对,passwordEncoder是当前类的一个成员变量

private PasswordEncoder passwordEncoder;

PasswordEncoder .java

public interface PasswordEncoder {


	/**
	 * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
	 * greater hash combined with an 8-byte or greater randomly generated salt.
	 */
	 //加密
	String encode(CharSequence rawPassword);

	/**
	 * Verify the encoded password obtained from storage matches the submitted raw
	 * password after it too is encoded. Returns true if the passwords match, false if
	 * they do not. The stored password itself is never decoded.
	 * @param rawPassword the raw password to encode and match
	 * @param encodedPassword the encoded password from storage to compare with
	 * @return true if the raw password, after encoding, matches the encoded password from
	 * storage
	 */
	 //比对密码
	boolean matches(CharSequence rawPassword, String encodedPassword);

	/**
	 * Returns true if the encoded password should be encoded again for better security,
	 * else false. The default implementation always returns false.
	 * @param encodedPassword the encoded password to check
	 * @return true if the encoded password should be encoded again for better security,
	 * else false.
	 */
	 //更新密码
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}

}

PasswordEncoder有众多的实现类,那么到底使用的是哪一种实现类的密码加密
在这里插入图片描述
通过断点可以得知使用的是一个叫做delegatingPasswordEncoder一个实现类,一种代理PasswordEncoder
在这里插入图片描述
进入matches方法
在这里插入图片描述
再次进入matches方法,跳转到DelegatingPasswordEncoder.java内的matches方法

@Override
	public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
		if (rawPassword == null && prefixEncodedPassword == null) {
			return true;
		}
		String id = extractId(prefixEncodedPassword);
		PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
		if (delegate == null) {
			return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
		}
		String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
		return delegate.matches(rawPassword, encodedPassword);
	}

在这里插入图片描述
断点方式得知,会将数据库中的密码前缀{noop}截取出来然后在Map中获得一个 NoOpPasswordEncoder (明文加密)
在这里插入图片描述

private final Map<String, PasswordEncoder> idToPasswordEncoder;

采用一种Map的方式存储PasswordEncoder,通过获取密码的加密方式然后在map中获取对应的PasswordEncoder,然后使用其PasswordEncoder的matches方法进行密码比对。

自然就会进入到NoOpPasswordEncoder.java直接使用equals进行密码比对

在这里插入图片描述
WebSecurityConfigurerAdapter.java
为AuthenticationManager设置PasswordEncoder ,会给一个defaultPasswordEncoder,然后通过LazyPasswordEncoder 类的PasswordEncoderFactories.createDelegatingPasswordEncoder()创建一个DelegatingPasswordEncoder

static class DefaultPasswordEncoderAuthenticationManagerBuilder extends AuthenticationManagerBuilder {

		private PasswordEncoder defaultPasswordEncoder;

		/**
		 * Creates a new instance
		 * @param objectPostProcessor the {@link ObjectPostProcessor} instance to use.
		 */
		DefaultPasswordEncoderAuthenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor,
				PasswordEncoder defaultPasswordEncoder) {
			super(objectPostProcessor);
			this.defaultPasswordEncoder = defaultPasswordEncoder;
		}

		@Override
		public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
				throws Exception {
			return super.inMemoryAuthentication().passwordEncoder(this.defaultPasswordEncoder);
		}

		@Override
		public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication() throws Exception {
			return super.jdbcAuthentication().passwordEncoder(this.defaultPasswordEncoder);
		}

		@Override
		public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
				T userDetailsService) throws Exception {
			return super.userDetailsService(userDetailsService).passwordEncoder(this.defaultPasswordEncoder);
		}

	}

	static class LazyPasswordEncoder implements PasswordEncoder {

		private ApplicationContext applicationContext;

		private PasswordEncoder passwordEncoder;

		LazyPasswordEncoder(ApplicationContext applicationContext) {
			this.applicationContext = applicationContext;
		}

		@Override
		public String encode(CharSequence rawPassword) {
			return getPasswordEncoder().encode(rawPassword);
		}

		@Override
		public boolean matches(CharSequence rawPassword, String encodedPassword) {
			return getPasswordEncoder().matches(rawPassword, encodedPassword);
		}

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

		private PasswordEncoder getPasswordEncoder() {
			if (this.passwordEncoder != null) {
				return this.passwordEncoder;
			}
			PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
			if (passwordEncoder == null) {
				passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
			}
			this.passwordEncoder = passwordEncoder;
			return passwordEncoder;
		}

		private <T> T getBeanOrNull(Class<T> type) {
			try {
				return this.applicationContext.getBean(type);
			}
			catch (NoSuchBeanDefinitionException ex) {
				return null;
			}
		}

		@Override
		public String toString() {
			return getPasswordEncoder().toString();
		}

	}

通过getPasswordEncoder方法和getBeanOrNull方法分析得知,如果在工厂中指定了PasswordEncoder的bean,就会使用指定PasswordEncoder,否则就会使用PasswordEncoderFactories.createDelegatingPasswordEncoder()方法去创建一个默认的DelegatingPasswordEncoder。

PasswordEncoderFactories.createDelegatingPasswordEncoder()方法,返回一个Map集合
PasswordEncoderFactories.java

public static PasswordEncoder createDelegatingPasswordEncoder() {
	String encodingId = "bcrypt";
	Map<String, PasswordEncoder> encoders = new HashMap<>();
	encoders.put(encodingId, new BCryptPasswordEncoder());
	encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
	encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
	encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
	encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
	encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
	encoders.put("scrypt", new SCryptPasswordEncoder());
	encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
	encoders.put("SHA-256",
			new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
	encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
	encoders.put("argon2", new Argon2PasswordEncoder());
	return new DelegatingPasswordEncoder(encodingId, encoders);
}

9.4 BCryptPasswordEncoder

9.4.1 测试加密算法(bcrypt)

 package com.study.test;import org.junit.jupiter.api.Test;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;/**
  * @ClassName TestPasswordEncoder
  * @Description TODO
  * @Author Jiangnan Cui
  * @Date 2022/8/25 21:31
  * @Version 1.0
  */
 public class TestPasswordEncoder {
     /**
      * @MethodName test
      * @Description BCrypt加密算法测试   
      * @Author Jiangnan Cui
      * @Date 21:36 2022/8/25
      */
     @Test
     public void test() {
         BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
         String encode = encoder.encode("123");
         System.out.println("encode = " + encode);
     }
 }

输出结果:每次运行加密后的结果都不一样,其中10表示10次散列

encode = $2a$10$mEyKn0XD5gN270okvgqUO.q8QZQbj9djTCtaPlcX1P9x74Rxv40Fu
encode = $2a$10$8MI7P6uSiWZLCojd2.eWvu2HvKsJj4fHReasv2hPvED0TFtnYmoYK
encode = $2a$10$/1tOejyIAmmeF2Ndlh/HYejFo5U8zKrVhw0YGbI6NRRRPEN1gW1Wa
              ↑
          默认10次散列

9.4.2 项目中修改测试

修改WebSecurityConfig.java中的加密算法

 package com.study.config;import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;/**
  * @ClassName WebSecurityConfig
  * @Description TODO
  * @Date 2022/8/25 20:49
  * @Version 1.0
  */
 @Configuration
 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     //设置内置数据源
     @Bean
     public UserDetailsService userDetailsService() {
         InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
         //此处的noop表示明文,会获取NoOpPasswordEncoder,此处的{noop}123表示密码123明文加密后的假想存储在数据库的密码
         //启动服务时使用用户名root、密码123进行登录验证
         //inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
         
         //此处的bcrypt表示bcrypt加密算法,会获取BCryptPasswordEncoder,密码123经bcrypt加密算法加密后密码其中之一为:$2a$10$mEyKn0XD5gN270okvgqUO.q8QZQbj9djTCtaPlcX1P9x74Rxv40Fu
         //{bcrypt}$2a$10$mEyKn0XD5gN270okvgqUO.q8QZQbj9djTCtaPlcX1P9x74Rxv40Fu表示密码123经bcrypt加密算法加密后的假想存储在数据库的密码
         //启动服务时使用用户名root、密码123进行登录验证
         inMemoryUserDetailsManager.createUser(
                 User.withUsername("root")
                         .password("{bcrypt}$2a$10$mEyKn0XD5gN270okvgqUO.q8QZQbj9djTCtaPlcX1P9x74Rxv40Fu")
                         .roles("admin").build());
         return inMemoryUserDetailsManager;
     }//使用全局自定义的AuthenticationManager
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService());
     }@Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .anyRequest().authenticated()
                 .and()
                 .formLogin()
                 .and()
                 .csrf().disable();
     }
 }

9.5 DelegatingPasswordEncoder

根据上面 PasswordEncoder的介绍,可能会以为 Spring Security中默认的密码加密方案应该是四种自适应单向加密函数中的一种,其实不然,在 spring Security 5.0之后,默认的密码加密方案其实是 DelegatingPasswordEncoder。从名字上来看,DelegatingPaswordEncoder是一个代理类,而并非一种全新的密码加密方案,DelegatingPasswordEncoder主要用来代理上面介绍的不同的密码加密方案。为什么采用DelegatingPasswordEncoder而不是某一个具体加密方式作为默认的密码加密方案呢?主要考虑了如下两方面的因素:

  • 兼容性:使用 DelegatingPasswordEncoder可以帮助许多使用旧密码加密方式的系统顺利迁移到 Spring security 中,它允许在同一个系统中同时存在多种不同的密码加密方案。

  • 便捷性:密码存储的最佳方案不可能一直不变,如果使用 DelegatingPasswordEncoder作为默认的密码加密方案,当需要修改加密方案时,只需要修改很小一部分代码就可以实现。

9.6 密码加密实战

  • 使用固定密码加密方案
    通过@Bean注解将BcryptPasswordEncoder注入到容器中,然后就会使用这个PasswordEncoder,就不会使用PasswordEncoderFactories.createDelegatingPasswordEncoder()方法去创建一个默认的DelegatingPasswordEncoder。
 @Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
     //使用自定义密码加密器替代默认的密码加密器
      @Bean
      public PasswordEncoder BcryptPasswordEncoder() {
          return new BCryptPasswordEncoder();
      }
     
      @Bean
     public UserDetailsService userDetailsService() {
         InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
           inMemoryUserDetailsManager.createUser(User.withUsername("root").password("$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6a").roles("xxx").build());
         return inMemoryUserDetailsManager;
     }
 }
  • 使用灵活密码加密方案(推荐)
    会根据密码的前缀去DelegatingPasswordEncoder获取密码指定的PasswordEncoder
 @Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
      @Bean
     public UserDetailsService userDetailsService() {
         InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
         inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{bcrypt}$2a$10$WGFkRsZC0kzafTKOPcWONeLvNvg2jqd3U09qd5gjJGSHE5b0yoy6a").roles("xxx").build());//使用默认的密码加密器  一定要返回id:bcrypt
         return inMemoryUserDetailsManager;
     }
 }

9.7加密升级实战Demo

推荐使用DelegatingPasswordEncoder的另外一个好处就是自动进行密码加密方案的升级,这个功能在整合一些老的系统时非常有用。

9.7.1 准备库表

 -- 用户表
 CREATE TABLE `user`
 (
     `id`                    int(11) NOT NULL AUTO_INCREMENT,
     `username`              varchar(32)  DEFAULT NULL,
     `password`              varchar(255) DEFAULT NULL,
     `enabled`               tinyint(1) DEFAULT NULL,
     `accountNonExpired`     tinyint(1) DEFAULT NULL,
     `accountNonLocked`      tinyint(1) DEFAULT NULL,
     `credentialsNonExpired` tinyint(1) DEFAULT NULL,
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;-- 角色表
 CREATE TABLE `role`
 (
     `id`      int(11) NOT NULL AUTO_INCREMENT,
     `name`    varchar(32) DEFAULT NULL,
     `name_zh` varchar(32) DEFAULT NULL,
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;-- 用户角色关系表
 CREATE TABLE `user_role`
 (
     `id`  int(11) NOT NULL AUTO_INCREMENT,
     `uid` int(11) DEFAULT NULL,
     `rid` int(11) DEFAULT NULL,
     PRIMARY KEY (`id`),
     KEY   `uid` (`uid`),
     KEY   `rid` (`rid`)
 ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

插入数据

插入数据

-- 插入用户数据
BEGIN;
  INSERT INTO `user`
  VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);
  INSERT INTO `user`
  VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);
  INSERT INTO `user`
  VALUES (3, 'blr', '{noop}123', 1, 1, 1, 1);
COMMIT;-- 插入角色数据
BEGIN;
  INSERT INTO `role`
  VALUES (1, 'ROLE_product', '商品管理员');
  INSERT INTO `role`
  VALUES (2, 'ROLE_admin', '系统管理员');
  INSERT INTO `role`
  VALUES (3, 'ROLE_user', '用户管理员');
COMMIT;-- 插入用户角色数据
BEGIN;
  INSERT INTO `user_role`
  VALUES (1, 1, 1);
  INSERT INTO `user_role`
  VALUES (2, 1, 2);
  INSERT INTO `user_role`
  VALUES (3, 2, 2);
  INSERT INTO `user_role`
  VALUES (4, 3, 3);
COMMIT;

9.7.2 整合mybatis

9.7.2.1 pom.xml中引入依赖
<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>5.1.38</version>
 </dependency><dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter</artifactId>
   <version>2.2.0</version>
 </dependency><dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid</artifactId>
   <version>1.2.8</version>
 </dependency>
9.7.2.2 application.properties中添加配置
 # datasource:类型、驱动名、用户名、密码
 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
 spring.datasource.username=root
 spring.datasource.password=root
 # mybatis配置mapper文件的位置和别名设置
 # 注意mapper目录(包)新建时必须使用"/",而不是.
 mybatis.mapper-locations=classpath:com/study/mapper/*.xml
 mybatis.type-aliases-package=com.study.entity
 # log:为了显示mybatis运行SQL语句
 logging.level.com.study=debug

9.7.3 编写实体类

User

 package com.study.entity;import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;import java.util.*;/**
  * @ClassName User
  * @Description TODO
  * @Date 2022/8/10 0:25
  * @Version 1.0
  */
 public class User implements UserDetails {
     private Integer id;
     private String username;
     private String password;
     private Boolean enabled;
     private Boolean accountNonExpired;
     private Boolean accountNonLocked;
     private Boolean credentialsNonExpired;
     private List<Role> roles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息//返回权限信息
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
         Set<SimpleGrantedAuthority> authorities = new HashSet<>();
         roles.forEach(role -> {
             SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
             authorities.add(simpleGrantedAuthority);
         });
         return authorities;
     }@Override
     public String getPassword() {
         return password;
     }public void setPassword(String password) {
         this.password = password;
     }@Override
     public String getUsername() {
         return username;
     }public void setUsername(String username) {
         this.username = username;
     }@Override
     public boolean isAccountNonExpired() {
         return accountNonLocked;
     }@Override
     public boolean isAccountNonLocked() {
         return accountNonLocked;
     }@Override
     public boolean isCredentialsNonExpired() {
         return credentialsNonExpired;
     }@Override
     public boolean isEnabled() {
         return enabled;
     }public Integer getId() {
         return id;
     }public void setId(Integer id) {
         this.id = id;
     }public Boolean getEnabled() {
         return enabled;
     }public void setEnabled(Boolean enabled) {
         this.enabled = enabled;
     }public Boolean getAccountNonExpired() {
         return accountNonExpired;
     }public void setAccountNonExpired(Boolean accountNonExpired) {
         this.accountNonExpired = accountNonExpired;
     }public Boolean getAccountNonLocked() {
         return accountNonLocked;
     }public void setAccountNonLocked(Boolean accountNonLocked) {
         this.accountNonLocked = accountNonLocked;
     }public Boolean getCredentialsNonExpired() {
         return credentialsNonExpired;
     }public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
         this.credentialsNonExpired = credentialsNonExpired;
     }public List<Role> getRoles() {
         return roles;
     }public void setRoles(List<Role> roles) {
         this.roles = roles;
     }
 }

Role

 package com.study.entity;/**
  * @ClassName Role
  * @Description TODO
  * @Date 2022/8/10 0:25
  * @Version 1.0
  */
 public class Role {
     private Integer id;
     private String name;
     private String nameZh;public Integer getId() {
         return id;
     }public void setId(Integer id) {
         this.id = id;
     }public String getName() {
         return name;
     }public void setName(String name) {
         this.name = name;
     }public String getNameZh() {
         return nameZh;
     }public void setNameZh(String nameZh) {
         this.nameZh = nameZh;
     }
 }

9.7.4 创建dao

 package com.study.dao;import com.study.entity.Role;
 import com.study.entity.User;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;import java.util.List;/**
  * @ClassName UserDao
  * @Description TODO
  * @Date 2022/8/27 10:05
  * @Version 1.0
  */
 @Mapper
 public interface UserDao {
     /**
      * @MethodName getRolesByUid
      * @Description 根据用户id查询角色
      * @param: uid
      * @return: java.util.List<com.study.entity.Role>
      * @Date 10:06 2022/8/27
      */
     List<Role> getRolesByUid(Integer uid);/**
      * @MethodName loadUserByUsername
      * @Description 根据用户名查询用户
      * @param: username
      * @return: com.study.entity.User
      * @Date 10:07 2022/8/27
      */
     User loadUserByUsername(String username);/**
      * @MethodName updatePassword
      * @Description 根据用户名更新密码
      * @param: username
      * @param: password
      * @return: java.lang.Integer
      * @Date 10:08 2022/8/27
      */
     Integer updatePassword(@Param("username") String username, @Param("password") String password);
 }

9.7.5 编写mapper

 <!DOCTYPE mapper
         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.study.dao.UserDao">
     <!--loadUserByUsername-->
     <select id="loadUserByUsername" resultType="User">
         select id,
                username,
                password,
                enabled,
                accountNonExpired,
                accountNonLocked,
                credentialsNonExpired
         from `user`
         where username = #{username}
     </select><!--getRolesByUid-->
     <select id="getRolesByUid" resultType="Role">
         select r.id,
                r.name,
                r.name_zh nameZh
         from `role` r,
              `user_role` ur
         where r.id = ur.rid
           and ur.uid = #{uid}
     </select><!--updatePassword-->
     <update id="updatePassword">
         update `user`
         set password=#{password}
         where username = #{username}
     </update></mapper>

9.7.6 编写service 实现

package com.study.service;import com.study.dao.UserDao;
 import com.study.entity.Role;
 import com.study.entity.User;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsPasswordService;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 import org.springframework.util.ObjectUtils;import java.util.List;/**
  * @ClassName MyUserDetailService
  * @Description TODO
  * @Date 2022/8/27 10:21
  * @Version 1.0
  */
 @Service
 public class MyUserDetailService implements UserDetailsService, UserDetailsPasswordService {
     //注入UserDao
     private final UserDao userDao;@Autowired
     public MyUserDetailService(UserDao userDao) {
         this.userDao = userDao;
     }/**
      * @MethodName loadUserByUsername
      * @Description 重写UserDetailsService接口中的loadUserByUsername,从数据库中加载用户
      * @Date 11:12 2022/8/27
      */
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         User user = userDao.loadUserByUsername(username);
         if (ObjectUtils.isEmpty(user)) {
             throw new RuntimeException("用户不存在!");
         }
         List<Role> roles = userDao.getRolesByUid(user.getId());
         user.setRoles(roles);
         return user;
     }/**
      * @MethodName updatePassword
      * @Description 重写UserDetailsPasswordService接口中的updatePassword,进行密码更新
      * @param: user
      * @param: newPassword
      * @return: org.springframework.security.core.userdetails.UserDetails
      * @Date 10:31 2022/8/27
      * 默认使用DelegatingPasswordEncoder 默认使用相当最安全密码加密 Bcrypt ----> Cxxx
      */
     @Override
     public UserDetails updatePassword(UserDetails user, String newPassword) {
         Integer result = userDao.updatePassword(user.getUsername(), newPassword);
         //根据更新的受影响的行数判断是否进行了更新,为1时表示更新成功
         if (result == 1) {
             ((User) user).setPassword(newPassword);//更新内存中用户的密码
         }
         return user;
     }
 }

9.7.7 配置securityconfig

 package com.study.config;import com.study.service.MyUserDetailService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;/**
  * @ClassName WebSecurityConfig
  * @Description TODO
  * @Date 2022/8/25 20:49
  * @Version 1.0
  */
 @Configuration
 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     //使用自定义数据源
     private final MyUserDetailService myUserDetailService;@Autowired
     public WebSecurityConfig(MyUserDetailService myUserDetailService) {
         this.myUserDetailService = myUserDetailService;
     }//使用自定义的AuthenticationManager
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(myUserDetailService);
     }//配置登录认证流程
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .anyRequest().authenticated()
                 .and()
                 .formLogin()
                 .and()
                 .csrf().disable();
     }
 }

启动项目,访问地址:http://localhost:8080/demo ,输入数据库中的数据(root、123;blr、123; admin、123)进行登录,登录成功后成功输出:Demo OK,同时数据库中的密码按照DelegatingPasswordEncoder默认的Bcrypt密码加密算法对密码进行了加密。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值