SpringSecurity学习总结1-入门
1. SpringSecurity入门
1.1 创建入门项目
我们先新建一个maven项目,我们使用SpringBoott方式,引入以下必要的依赖。
<parent> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-parentartifactId> <version>2.1.4.RELEASEversion> parent> <dependencies> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-securityartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-testartifactId> <scope>testscope> <exclusions> <exclusion> <groupId>org.junit.vintagegroupId> <artifactId>junit-vintage-engineartifactId> exclusion> exclusions> dependency> dependencies>
编写一个SpringBoot项目的启动类
@SpringBootApplication public class SpringSecurityApplication { public static void main(String[] args) { try { SpringApplication.run(SpringSecurityApplication.class, args); } catch (Exception e) { e.printStackTrace(); } } }
在resources目录下,新建一个文件夹static,在static下创建一个 index.html文件
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页title>
head>
<body>
登录成功!!
body>
html>
1.2 测试入门项目
我们运行启动类 SpringSecurityApplication,当项目启动之后,启动日志里会有这样一句日志(说明SpringSecurity已经生效,默认登录用户名是user, 默认密码每次启动都不一样)
Using generated security password: afa0c4a7-9108-42b7-9d40-b663920cb72d
当项目启动后,输入请求地址:http://localhost:8080/ 浏览器自动跳转到地址 http://localhost:8080/login 会看到一个登录页面。
这个登录页面是SpringSecurity自带的,输入日志里打印的username和password之后,登录成功,浏览器会挑战到我们自己写的index.html页面。
2. Spring Security的基础方法
2.1 UserDetailsService接口
package org.springframework.security.core.userdetails; public interface UserDetailsService { /** * Locates the user based on the username. In the actual implementation, the search * may possibly be case sensitive, or case insensitive depending on how the * implementation instance is configured. In this case, theUserDetails* object that comes back may have a username that is of a different case than what * was actually requested.. * * @param username the username identifying the user whose data is required. * * @return a fully populated user record (nevernull) * * @throws UsernameNotFoundException if the user could not be found or the user has no * GrantedAuthority */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
UserDetailsService接口是Spring Security的主要类,后面我们需要继承这个接口,来实现自己的业务逻辑。
其中loadUserByUsername(String username)是用来实现登录逻辑的,参数username就是登录用户名,也就是刚才我们填写的user,如果用户名不存在会报UsernameNotFoundException异常,返回值是UserDetails对象。
2.1 UserDetails接口
package org.springframework.security.core.userdetails; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import java.io.Serializable; import java.util.Collection; public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
UserDetails也是一个接口类,以后我们也可以让自己的用户类实现UserDetails的方法,来完成自己的业务逻辑。
UserDetails继承了Serializable,说明可以被序列化。
Collection<? extends GrantedAuthority> getAuthorities(); 此方法用于获取用户权限的,并且结果不能为null
String getPassword(); 获取用户的密码。
String getUsername(); 获取用户名。
boolean isAccountNonExpired(); 判断账号是否未过期,过期的账号无法进行认证。
boolean isAccountNonLocked(); 判断账号是否未被锁定。被锁定的用户无法进行认证。
boolean isCredentialsNonExpired(); 判断用户的凭证(密码)是否未过期,过期的凭据无法进行身份验证。
boolean isEnabled(); 账号是否被启动,未启动的用户无法进行认证。
2.3 User类
UserDetails是一个接口,所以不能直接使用,SpringSecurity提供了一个实现类User
package org.springframework.security.core.userdetails; public class User implements UserDetails, CredentialsContainer { private String password; private final String username; private final Setauthorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities); } public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { if (((username == null) || "".equals(username)) || (password == null)) { throw new IllegalArgumentException( "Cannot pass null or empty values to constructor"); } this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); } }
User继承了UserDetails,提供了UserDetails的7条属性和2个构造方法。User(String username, String password, Collection<? extends GrantedAuthority> authorities) 这三个参数的构造方法实际上是调用了下面7个参数的构造方法,传的参数就是username、password和authorities。
当UserDetailsService的loadUserByUsername方法显示登录认证之后,会在数据库或内存中获取到username、password、authorities赋值给UserDetails的实现类User,并返回,实现了完整的登录逻辑。
2.4 PsswordEncoder 密码加密
接下来我们认识一下密码验证的核心接口 PasswordEncoder,
密码解析器,接口,
package org.springframework.security.crypto.password; public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); default boolean upgradeEncoding(String encodedPassword) { return false; } }
String encode(CharSequence rawPassword); 密码加密方法,参数rawPassword可以理解为客户端的明文密码,SpringSecurity推荐使用SHA-1或者Hash算法加密,Hash算法推荐使用8位字符或随机salt。
boolean matches(CharSequence rawPassword, String encodedPassword); 密码匹配方法,rawPassword是明文密码,encodedPassword是加密后的密码,匹配这两个密码是否一致。
default boolean upgradeEncoding(String encodedPassword) 二次加密方法,对已加密的密码,再次加密。默认返回false是不需要二次加密。
PasswordEncoder是接口,它也有很多实现类,官方推荐使用的实现类是 BCryptPasswordEncoder
public class BCryptPasswordEncoder implements PasswordEncoder { private Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}"); private final Log logger = LogFactory.getLog(getClass()); private final int strength; private final SecureRandom random; public BCryptPasswordEncoder() { this(-1); } /** * @param strength the log rounds to use, between 4 and 31 */ public BCryptPasswordEncoder(int strength) { this(strength, null); } /** * @param strength the log rounds to use, between 4 and 31 * @param random the secure random instance to use * */ public BCryptPasswordEncoder(int strength, SecureRandom random) { if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) { throw new IllegalArgumentException("Bad strength"); } this.strength = strength; this.random = random; } public String encode(CharSequence rawPassword) { String salt; if (strength > 0) { if (random != null) { salt = BCrypt.gensalt(strength, random); } else { salt = BCrypt.gensalt(strength); } } else { salt = BCrypt.gensalt(); } return BCrypt.hashpw(rawPassword.toString(), salt); } public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword == null || encodedPassword.length() == 0) { logger.warn("Empty encoded password"); return false; } if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) { logger.warn("Encoded password does not look like BCrypt"); return false; } return BCrypt.checkpw(rawPassword.toString(), encodedPassword); } }
BCryptPasswordEncoder是一个强哈希加密方法。
private final int strength; 此参数决定了密码强度,默认是10。如果是10以上的数字,密码会更加安全,但是会在加密过程消耗更多的性能。
public String encode(CharSequence rawPassword) 对明文密码加密,加密时使用了随机salt(随机字符串),保证每次加密结果不一样。(注:如果没有使用随机salt,相同字符串加密后的结果是一样,就很容易猜到密码,很不安全)
我们可以写个测试用例试一下,多运行几次会发现每次加密的结果是不一样的,这就是随机salt起作用了。
@RunWith(SpringRunner.class) @SpringBootTest(classes = SpringSecurityApplication.class) public class SpringSecurityTest { @Test public void checkPassword(){ PasswordEncoder pw = new BCryptPasswordEncoder(); // 加密 String encode = pw.encode("123"); System.out.println("==== 加密后的密码:" + encode); // 比较密码 boolean matches = pw.matches("123", encode); System.out.println("==== 比较密码:" + matches); } }
3. 登录功能实现
3.1 配置密码解析器
当我们要实现自定义登录时,Spring容器内必须已经存在密码解析器,所以我们要提前把PasswordEncoder写到配置类里面去,让Spring来管理。
@Configuration public class SecurityConfig { /** * 密码解析器 */ @Bean public PasswordEncoder getPasswordEncoder(){ return new BCryptPasswordEncoder(); } }
3.2 实现登录验证
创建一个类,实现UserDetailsService接口。正常情况下用户密码都要从数据库查询的,我这里为了测试方便,直接写死的。
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 根据用户名去数据库查询,如果不存在就抛UsernameNotFoundException异常 if(!"admin".equals(username)){ throw new UsernameNotFoundException("用户名不存在"); } // 2. 比较密码(注册时已经加密过,如果匹配成功返回UserDetails) String password = passwordEncoder.encode("123"); // 正常逻辑是需要使用 passwordEncoder.matches() 方法来验证密码的是否正确的。 return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal")); } }
配置完毕后,我们再次启动应用程序,发现日志里不打印默认用户名密码的,在登录页面只能输入 username=admin, password=123 才可以登录。