4.2.2.1 AuthenticationProvider
通过前面的Spring Security认证流程我们得知,认证管理器(AuthenticationManager)委托 AuthenticationProvider 完成认证工作。
AuthenticationProvider是一个接口 ,定义如下:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.authentication; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; public interface AuthenticationProvider { Authentication authenticate(Authentication var1) throws AuthenticationException; boolean supports(Class<?> var1); }
authenticate()方法定义了认证的实现过程,它的参数是一个Authentication ,里面包含了登录用户所提交的用户 密码等。而返回值也是一个Authentication ,这个Authentication则是在认证成功后,将用户的权限及其他信息重新组装后生成。
Spring Security中维护着一个List列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProviderl ,短信登录时使用 Authentication Provider2等等这样的例子很多。
每个AuthenticationProvider需要实现supports ()方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken ,它是一个Authentication ,里面封装着用户提交的用户名、密码信息。
而对应哪个AuthenticationProvider来处理它?
我们在 DaoAuthenticationProvider的基类 AbstractUserDetailsAuthenticationProvider 发现以下代码:
public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); }//149行
也就是说当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理。
最后,我们来看一下Authentication(认证信息)的结构,它是一个接口 ,我们之前提到的 UsernamePasswordAuthenticationToken就是它的实现之一:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.core; import java.io.Serializable; import java.security.Principal; import java.util.Collection; public interface Authentication extends Principal, Serializable {//1 Collection<? extends GrantedAuthority> getAuthorities();//2 Object getCredentials();//3 Object getDetails();//4 Object getPrincipal();//5 boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException; }
(1 ) Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于 java.security 包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。
(2 ) getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
(3 ) getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
(4 ) getDetails(),细节信息,web应用中的实现接口通常为WebAuthenticationDetails ,它记录了访问者的ip地址和sessionld的值。
(5 ) getPrincipal(),身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细信息,从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。
附参考代码:
/* * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved. * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. * */ package java.security; import javax.security.auth.Subject; /** * This interface represents the abstract notion of a principal, which * can be used to represent any entity, such as an individual, a * corporation, and a login id. * * @see java.security.cert.X509Certificate * * @author Li Gong */ public interface Principal { /** * Compares this principal to the specified object. Returns true * if the object passed in matches the principal represented by * the implementation of this interface. * * @param another principal to compare with. * * @return true if the principal passed in is the same as that * encapsulated by this principal, and false otherwise. */ public boolean equals(Object another); /** * Returns a string representation of this principal. * * @return a string representation of this principal. */ public String toString(); /** * Returns a hashcode for this principal. * * @return a hashcode for this principal. */ public int hashCode(); /** * Returns the name of this principal. * * @return the name of this principal. */ public String getName(); /** * Returns true if the specified subject is implied by this principal. * * <p>The default implementation of this method returns true if * {@code subject} is non-null and contains at least one principal that * is equal to this principal. * * <p>Subclasses may override this with a different implementation, if * necessary. * * @param subject the {@code Subject} * @return true if {@code subject} is non-null and is * implied by this principal, or false otherwise. * @since 1.8 */ public default boolean implies(Subject subject) { if (subject == null) return false; return subject.getPrincipals().contains(this); } }
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.core; import java.io.Serializable; public interface GrantedAuthority extends Serializable { String getAuthority(); }
4.2.2.2 UserDetailsService
DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后既得到一个Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal) 。 这个身份信息就是一个object ,大多数情况下它可以被强转为UserDetails对象。
DaoAuthenticationProvider中包含了一个UserDetailsService实例,它负责根据用户名提取用户信息UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的UserDetailsService公开为spring bean来自定义身份验证。
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.core.userdetails; public interface UserDetailsService { UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; }
很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认证流程,同时会把UserDetails填充至Authentication。
上面一直提到UserDetails是用户信息:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.core.userdetails; import java.io.Serializable; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
它和 Authentication 接口很类似,比如它们都拥有 username ,authorities。Authentication 的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。Authentication接口中的getDetails()方法,其中的UserDetails用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。
通过实现UserDetailsService和UserDetails ,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
Spring Security提供的lnMemoryllserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。
测试
自定义UserDetailsService
package com.uncle.seciruty.springboot.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; /** * @program: spring-boot-security * @description: * @author: 步尔斯特 * @create: 2021-08-06 22:05 */ @Service public class SpringDataUserDetailsService implements UserDetailsService { @Autowired UserDao userDao; //根据 账号查询用户信息 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //将来连接数据库根据账号查询用户信息 UserDto userDto = userDao.getUserByUsername(username); if(userDto == null){ //如果用户查不到,返回null,由provider来抛出异常 return null; } //根据用户的id查询用户的权限 List<String> permissions = userDao.findPermissionsByUserId(userDto.getId()); //将permissions转成数组 String[] permissionArray = new String[permissions.size()]; permissions.toArray(permissionArray); UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray).build(); return userDetails; } }
屏蔽安全配置类中UserDetailsService的定义
package com.uncle.seciruty.springboot.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; /** * @program: spring-boot-security * @description: * @author: 步尔斯特 * @create: 2021-07-23 19:40 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //定义用户信息服务(查询用户信息) // @Bean // public UserDetailsService userDetailsService(){ // InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); // manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build()); // manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build()); // return manager; // } //密码编码器 @Bean public PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); } //安全拦截机制(最重要) @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/r/r1").hasAuthority("p1") .antMatchers("/r/r2").hasAuthority("p2") .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过 .anyRequest().permitAll()//除了/r/**,其它的请求可以访问 .and() .formLogin()//允许表单登录 .successForwardUrl("/login-success");//自定义登录成功的页面地址 } }
重启工程,请求认证,SpringDataUserDetailsService的loadUserByUsername方法被调用,查询用户信息。
4.2.2.3 PasswordEncoder
DaoAuthenticationProvide以证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求 Authentication中的密码做对比呢?在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过 PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.crypto.password; public interface PasswordEncoder { String encode(CharSequence var1); boolean matches(CharSequence var1, String var2); default boolean upgradeEncoding(String encodedPassword) { return false; } }
而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如下声明即可,如下:
@Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
1、用户输入密码(明文)
2、DaoAuthenticationProvider获取UserDetails (其中存储了用户的正确密码)
3、DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。
NoOpPasswordEncode啲校验规则拿输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则校验失败。
实际项目中推荐使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder,SCryptPasswordEncoder等。
使用BCryptPasswordEncoder
1、 配置BCryptPasswordEncoder 在安全配置类中定义:
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
测试发现认证失败,提示:Encoded password does not look like BCrypt。
public boolean matches(CharSequence rawPassword, String encodedPassword) { if (rawPassword == null) { throw new IllegalArgumentException("rawPassword cannot be null"); } else if (encodedPassword != null && encodedPassword.length() != 0) { if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) { this.logger.warn("Encoded password does not look like BCrypt"); return false; } else { return BCrypt.checkpw(rawPassword.toString(), encodedPassword); } } else { this.logger.warn("Empty encoded password"); return false; } }
原因:
由于UserDetails中存储的是原始密码(比如:123 ),它不是BCrypt格式。 跟踪DaoAuthenticationProvider第33行代码查看userDetails中的内容,跟踪第38行代码查看 Password Encoder的类型。
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) {//33行 this.logger.debug("Failed to authenticate since no credentials provided"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { String presentedPassword = authentication.getCredentials().toString(); if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {//38行 this.logger.debug("Failed to authenticate since password does not match stored value"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } }
测试BCrypt
通过下边的代码测试BCrypt加密及校验的方法
添加依赖:
<dependency> <groupld>org.springframework.boot</groupld> <artifactld>spring-boot-starter-test</artifactld> <scope>test</scope> </dependency>
编写测试方法:
package com.uncle.seciruty.springboot.util; import org.springframework.security.crypto.bcrypt.BCrypt; /** * @program: spring-boot-security * @description: * @author: 步尔斯特 * @create: 2021-08-06 22:12 */ public class BCryptUtil { public static void main(String[] args) { String hashpw = BCrypt.hashpw("456", BCrypt.gensalt()); System.out.println(hashpw); boolean checkpw = BCrypt.checkpw("456","$2a$10$bcJXXryMCxXtkxRkG1UekOkOe0BqxiqOYKJzGni64jnyWAD15wmDy"); System.out.println(checkpw); //123 -> $2a$10$8iHn2TEvyzkUgO2np9glzufe.wtRyjA5u3xfvs/D.9FCzm1XvCAGm //456 } }
修改安全配置类
将UserDetails中的原始密码修改为BCrypt格式:
//密码编码器 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
实际项目中存储在数据库中的密码并不是原始密码,都是经过加密处理的密码。
4.2.3 授权流程
Spring Security可以通过http.authorizeRequests()对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。
Spring Security的授权流程如下:
分析授权流程:
1.拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的FiltersecurityInterceptor的子类拦截。
2.获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterlnvocationSecurityMetadataSource获取要访问当前资源所需要的权限Collection<ConfigAttribute> 。
http .authorizeRequests() .antMatchers("/r/r1").hasAuthority("p1") .antMatchers("/r/r2").hasAuthority( "p2")
3.Filtersecurityinterceptor会调用AccessDecisionManager进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.access; import java.util.Collection; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; public interface AccessDecisionManager { /** *通过传递的参数来决定用户是否有访问对应受保护资源的权限 */ void decide(Authentication var1, Object var2, Collection<ConfigAttribute> var3) throws AccessDeniedException, InsufficientAuthenticationException; boolean supports(ConfigAttribute var1); boolean supports(Class<?> var1); }
这里着重说明一下decide的参数:
authentication :要访问资源的访问者的身份
object :要访问的受保护资源,web请求对应Fi足revocation
configAttributes :是受保护资源的访问策略,通过SecurityMetadataSource获取。
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
授权决策
AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。
AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。
AccessDecisionVoter是一个接口 ,其中定义有三个方法,具体结构如下所示。
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.access; import java.util.Collection; import org.springframework.security.core.Authentication; public interface AccessDecisionVoter<S> { int ACCESS_GRANTED = 1; int ACCESS_ABSTAIN = 0; int ACCESS_DENIED = -1; boolean supports(ConfigAttribute var1); boolean supports(Class<?> var1); int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3); }
vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。
- ACCESS_GRANTED表示同意
- ACCESS_DENIED表示拒绝
- ACCESS_ABS7AIN表示弃权
如果一个AccessDecisionVoter不能判定当前 Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。
Spring Security内置了三个基于投票的AccessDecisionManager实现类,它们分别是
AffirmativeBased、ConsensusBased和UnanimousBased。
AffirmativeBased的逻辑是:
- 只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
- 如果全部弃权也表示鮑;
- 如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException, Spring security默认使用的是AffirmativeBased。
ConsensusBased的逻辑是:
- 如果赞成票多于反对票则表示通过。
- 反过来,如果反对票多于赞成票则将抛出AccessDeniedException,
- 如果赞成票与反对票相同且不等于0 ,并且属性allowlfEqualGrantedDeniedDecisions的值为true,则表示通过,否则将抛出异常AccessDeniedException。参数allowlfEqualGrantedDeniedDecisions的值默认为true。
- 如果所有的AccessDecisionVoter都弃权了,则将视参数allowlfAIIAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedExceptiono。参数allowlfAIIAbstainDecisions的值默认为false。
UnanimousBased
UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。
UnanimousBased的逻辑具体来说是这样的:
如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出 AccessDeniedExceptiono
如果没有反对票,但是有赞成票,则表示通过。
如果全部弃权了,则将视参数allowlfAIIAbstainDecisions的值而定,true则通过,false则抛出 AccessDeniedExceptiono SpringSecurity也内置一些投票者实现类如RoleVoter、AuthenticatedVoter和WebExpressionVoter等,可以自行查阅资料进行学习。
4.3 自定义认证
Spring Security提供了非常好的认证扩展方法,比如:将用户信息存储到内存中,实际开发中用户信息通常在数据库,Spring Security可以实现从数据库读取用户信息,Spring security还支持多种授权方法。
4.3.1 自定义登录页面
Spring Security的默认配置没有明确设定一个登录页面的URL ,因此Spring Security会根据启用的功能自动生成一个登录页面URL ,并使用默认URL处理登录的提交内容,登录后跳转的到默认URL等等。尽管自动生成的登录页面很方便快速启动和运行,但大多数应用程序都希望定义自己的登录页面。
4.3.1.1 认证页面
将security-springmvc工程的login.jsp拷贝到security-springboot下,目录保持一致。
在这里插入代码片
4.3.1.2 配置认证页面
在WebConfig.java中配置认证页面地址:
//默认Url根路径剧阵专至Ij/login ,此url为spring security提供 @0verride public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("login-view") registry.addViewController("login-view").setViewName("login"); }
4.3.1.3 安全配置
在WebSecurityConfig中配置表单登录信息:
(1)允许表单登录
(2 )指定我们自己的登录页,spring security以重定向方式将路径转发到/login-view
(3)指定登录处理的URL ,也就是用户名、密码表单提交的目的路径
(4)指定登录成功后的跳转URL
(5 )我们必须允许所有用户访问我们的登录页(例如为验证的用户),这个formLogin() .permitAll()方法允许任意用户访问基于表单登录的所有的URL。
4.3.1.4 测试
当用户没有认证时访问系统的资源会重定向到login-view页面
在这里插入代码片
输入账号和密码,点击登录,报错:
在这里插入代码片
问题解决:
spring security为防止CSRF ( Cross-site request forgery跨站请求伪造)的发生,限制了除了get以外的大多数方 法。
解决方法1:
屏蔽CSRF控制,即spring security不再限制CSRF。
配置 WebSecurityConfig
@0verride protected void configure(HttpSecurity http) throws Exception { http.csrf().disable()//屏蔽CSRF控制,即 spring security 不再限制CSRF }
解决办法2 :
在login.jsp页面添加一个token , spring security会验证token ,如果token合法则可以继续请求。
修改 login.jsp
<form action="login" method="post"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> </form>
4.3.2 连接数据库认证
前边的例子我们是将用户信息存储在内存中,实际项目中用户信息存储在数据库中。根据前边对认证流程研究,只需要重新定义UserDetailService即可实现根据用户账号查询数据库。
4.3.2.1 创建数据库
创建user_db数据库
CREATE DATABASE ‘user_db’ CHARACTER SET 'utf8‘ COLLATE ‘utf8_general_ci’;
创建t_user 表
CREATE TABLE 't_user' ( 'id' bigint(20) NOT NULL COMMENT,用户id', 'username' varchar(64) NOT NULL, 'password' varchar(64) NOT NULL, 'fullname' varchar(255) NOT NULL COMMENT,用户姓名', 'mobile' varchar(ll) DEFAULT NULL COMMENT,手机号 PRIMARY KEY ('id') USING BTREE )ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC
4.3.2.2 代码实现
1 )定义dataSource
在application.properties 配置
spring.datasource.url=jdbc:mysql://localhost:3306/user_db spring.datasource.username=root spring.datasource.password=mysql spring.datasource.driver-class-name=com.mysql.jdbc.Driver
2)添加依赖
<dependency> <groupld>org.springframework.boot</groupld> <artifactld>spring-boot-starter-test</artifactld> <scope>test</scope> </dependency> <dependency> <groupld>org.springframework.boot</groupld> <artifactld>spring-boot-starter-jdbc</artifactld> </dependency> <dependency> <groupld>mysql</groupld> <artifactld>mysql-connector-java</artifactld> <version>5.1.47</version> </dependency>
3 )定义Dao
定义模型类型,在model包定义UserDto:
@Data public class UserDto { private String id; private String username; private String password; private String fullname; private String mobile; }
在Dao包定义UserDao :
@Repository public class UserDao { @Autowired ZJdbcTemplate jdbcTemplate; public UserDto getUserByUsername(String username){ String sql =r,select id,username,password,funname from t_user where username = ?"; List<UserDto> list = jdbcTemplate.query(sqlj new Object[Jlusername}^ new BeanPropertyRowMappero(UserDto. class)); if(list == null && list.size() <= 0)( return null; } return list.get(0); } }
4.3.2.3 定义 UserDetailService
@Service public class SpringDatallserDetailsService implements UserDetailsService { @Autowired UserDao userDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //登录账号 System.out. println(); //根据账号去数据库查询... UserDto user = userDao.getUserByUsername(username); if(user == null){ return null; } //这里暂时使用静态麴居 UserDetails userDetails =User.withUsername(user.getFullname()).password(user.getPassword()).authorities("pl").build(); return userDetails; } }
4.3.2.3 测试
输入账号和密码请求认证,跟踪代码。
4.3.2.4 使用BCryptPasswordEncoder
按照我们前边讲的PasswordEncoder的使用方法,使用BCryptPasswordEncoder需要完成如下工作:
1、在安全配置类中定义BCryptPasswordEncoder
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
2、UserDetails中的密码存储BCrypt格式
前边实现了从数据库查询用户信息,所以数据库中的密码应该存储BCrypt格式
package com.uncle.seciruty.springboot.util; import org.springframework.security.crypto.bcrypt.BCrypt; /** * @program: spring-boot-security * @description: * @author: 步尔斯特 * @create: 2021-08-06 22:12 */ public class BCryptUtil { public static void main(String[] args) { String hashpw = BCrypt.hashpw("456", BCrypt.gensalt()); System.out.println(hashpw); boolean checkpw = BCrypt.checkpw("456","$2a$10$bcJXXryMCxXtkxRkG1UekOkOe0BqxiqOYKJzGni64jnyWAD15wmDy"); System.out.println(checkpw); //123 -> $2a$10$8iHn2TEvyzkUgO2np9glzufe.wtRyjA5u3xfvs/D.9FCzm1XvCAGm //456 -> $2a$10$bcJXXryMCxXtkxRkG1UekOkOe0BqxiqOYKJzGni64jnyWAD15wmDy } }
4.4 会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring
security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
4.4.1 获取用户身份
编写LoginController,实现/r/r1、 /r/r2的测试资源,并修改loginSuccess方法,注意getUsername方法,Spring Security 获取当前登录用户信息的方法为 SecurityContextHolder.getContext().getAuthentication()
@RestController public class Logincontroller { /** *用户登录成功 * @return */ @RequestMapping( value = "/login-success", produces = {"text/plain; charset=UTF-8"}) public String loginSuccess(){ String username = getUsername(); return username + "登录成功"; } /** *获取当前登录用户名 * @return */ private String getUsername(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(!authentication.isAuthenticated()){ return null; } Object principal = authentication.getPrincipal(); String username = null; if (principal instanceof org.springframework.security.core.userdetails.UserDetails) { username =((org.springframework.security.core.userdetails.UserDetails)principal).getUsername(); } else { username = principal.toString(); } return username; }
测试
登录前访问资源 被重定向至登录页面。
登录后访问资源 成功访问资源,如下:
zhang san访冋资源1