目录
1.每个实体类的属性名都不一样,怎么知道你哪个字段表示用户名,那个字段表示密码,Spring Security提供了统一的方法实现UserDetails 接口
2.new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());:拿到用户名和密码构建登录令牌
3.authenticationManager.authenticate(token);//这一步就是去执行登录
4.自定义登录接口要手动存信息进SecurityContextHolder
1.注册LoginController的AuthenticationManager的认证器到spring中去
2.configure(HttpSecurity http)过滤登录接口
内部源码:PasswordEncoder :Ctrl+h可以查看他的实现类
2.matches(明文密码,加密密码)密码匹对:登录会自动帮你执行
测试BCryptPasswordEncoder加密:密码自带盐值
new BCryptPasswordEncoder(10);可以设置密码强度,加密起来慢,对于破解也慢了
06、Spring Security新的加密方法逐步替代原有的加密方法
1.如果我们没有手动配置密码加密工具,那么默认PasswordEncoder,实际上是DelegatingPasswordEncoder
2.DelegatingPasswordEncoder 代理了所有密码加密器,他会根据密码的字符串格式,而选择一个合适的加密工具
1.密码升级,实现UserDetailsPasswordService 接口
2.重新方法: *当用户登录的时候,会去自动检查当前用户密码是不是bcrypt,如果不是,则会自动进行密码升级,那么就会触发该方法
1.首先要有passwordEncoder 统一的密码加密类型
2.重写PasswordEncoderFactories加入@Bean改变密码强度
04、Spring Security从数据库中读取信息
pom依赖
<!-- Spring Security框架--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <!-- druid数据源--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.9</version> </dependency> <!-- mysql连接数据库--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency><!--资源过滤--> <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
application.properties
spring.datasource.username=root spring.datasource.password=root spring.datasource.url=jdbc:mysql:///student02?serverTimezone=Asia/Shanghai spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
User
1.每个实体类的属性名都不一样,怎么知道你哪个字段表示用户名,那个字段表示密码,Spring Security提供了统一的方法实现UserDetails 接口
2.先重写方法,再实现get和set方法
3.重写了自动的,记得删掉get方法
public class User implements UserDetails {
private Integer id; private String username; private String nickname; private String password; private Boolean enabled; private Integer role; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public void setUsername(String username) { this.username = username; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public void setPassword(String password) { this.password = password; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public Integer getRole() { return role; } public void setRole(Integer role) { this.role = role; } =========================================== /** * 返回用户的 角色/权限 * @return */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } /** * 获取用户的密码:放回上边密码字段 * @return */ @Override public String getPassword() { return password; } /** * 获取用户名::放回上边用户名 * @return */ @Override public String getUsername() { return username; } /** * 账户是否没有过期 * @return */ @Override public boolean isAccountNonExpired() { return true; } /** * 账户是否没有被锁定 * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 密码是否没有过期 * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 账户是否可用:如果上面有自动就如实放回 * @return */ @Override public boolean isEnabled() { return enabled; } }
UserService
当用户登录的时候,会自动调用到这个方法
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 根据用户名查询用户对象
* 当用户登录的时候,会自动调用到这个方法
* @param username 登录的时候,用户输入的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
//说明用户名不存在,这个异常最终会被隐藏起来,转而抛出 BadCredentialsException
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}
UserMapper
@Mapper public interface UserMapper { User loadUserByUsername(String username); }
UserMapper.xml
<mapper namespace="com.qfedu.security02.mapper.UserMapper"> <select id="loadUserByUsername" resultType="com.qfedu.security02.model.User"> select * from user where username=#{username}; </select> </mapper>
05、Spring Security自定义登录接口
LoginController
1.AuthenticationManager 就是 Spring Security 中的认证器,这是一个接口,这个接口只有一个实现类,ProviderManager,用户登录时侯具体的校验工作,就是由它来完成的。
2.new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());:拿到用户名和密码构建登录令牌
3.authenticationManager.authenticate(token);//这一步就是去执行登录
4.自定义登录接口要手动存信息进SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(token.getPrincipal(), null, token.getAuthorities()));
5.登录失败抛出的异常
@RestController public class LoginController { /** * AuthenticationManager 就是 Spring Security 中的认证器,这是一个接口,这个接口只有一个实现类,ProviderManager,用户登录时侯具体的校验工作,就是由它来完成的。 */ @Autowired AuthenticationManager authenticationManager; /** * 不使用 Spring Security 自带的登录接口,而是自己写一个登录接口 * * @param user * @return */ @PostMapping("/login") public Map<String, Object> login(@RequestBody User user) { Map<String, Object> map = new HashMap<>(); //构建登录令牌 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()); try { //这一步就是去执行登录,类似于 shiro 中的 subject.login 方法 authenticationManager.authenticate(token); //将当前登录成功的用户信息存入到 SecurityContextHolder 中 SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(token.getPrincipal(), null, token.getAuthorities())); map.put("status", "200"); map.put("message", "登录成功"); } catch (AuthenticationException exception) { //登录失败 map.put("status", "500"); if (exception instanceof BadCredentialsException) { map.put("message", "用户名或者密码写错,登录失败"); } else if (exception instanceof UsernameNotFoundException) { //出于安全考虑,Spring Security 内部将 UsernameNotFoundException 隐藏起来了,转而抛出了 BadCredentialsException } else if (exception instanceof AccountExpiredException) { map.put("message", "账户过期,登录失败"); } else if (exception instanceof CredentialsExpiredException) { map.put("message", "密码过期,登录失败"); } else if (exception instanceof DisabledException) { map.put("message", "账户被禁用,登录失败"); } else if (exception instanceof LockedException) { map.put("message", "账户被锁定,登录失败"); } } return map; }
SecurityConfig
1.注册LoginController的AuthenticationManager的认证器到spring中去
2.configure(HttpSecurity http)过滤登录接口
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() //从 Spring Security 过滤器链中移除 CsrfFilter .csrf().disable(); } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
06、Spring Security密码加密
内部源码:PasswordEncoder :Ctrl+h可以查看他的实现类
1.encode密码加密
2.matches(明文密码,加密密码)密码匹对:登录会自动帮你执行
3.upgradeEncoding密码升级
4.自带盐值,相同的明文加密成密码不同
public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); default boolean upgradeEncoding(String encodedPassword) { return false; } }
Security02ApplicationTests
测试BCryptPasswordEncoder加密:密码自带盐值
new BCryptPasswordEncoder(10);可以设置密码强度,加密起来慢,对于破解也慢了
@SpringBootTest class Security02ApplicationTests { @Test void contextLoads() { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10); for (int i = 0; i < 10; i++) { System.out.println(encoder.encode("123")); } } }$2a$10$NqbXkorf.Df13lefBvZspuD9wHj9CH3KUAYQCOBvqDRyze8XPVck6
$2a$10$kFIsIVV1y0Zuvs1cggzqnuZ7Qzgq0whzf3ozf0iGmbAxvBi731mji
$2a$10$giZViF92vIsZF6jrlERRB.lJm2XcMU8H/u3wox3QWCBDy9aLukcGK
$2a$10$UtWGZsdlBiLatF1CGGFC.uXibKwqydjS1Nb0ob5fGuVZyHi//NjwK
$2a$10$.TsjcfVJ0RYfAkREUM.RE.yfKrPrskRWflDjyxbo83QjkOKzsmU2C
$2a$10$lDppXh3Z32Q3qlpKdpXB8OP6UKKwRPlNqRV1CBtJ/BWl8XEuHh1a6
$2a$10$jpEDlzw36HGaSibKJpeu.uAn0YU9BhhjFrgoLAd8MGE1bkdr7zMW2
$2a$10$is5sKcAajPNQTTfojjjJvuSlY7s1VAe5kcyDoa7pWCGcmwETKglVi
$2a$10$eMoBXb3slmXSl/OM9Rjg2O1AYKZQdOtECZzh01YwmhDLxQbBrcwgi
$2a$10$wZP9XblkBXVwMdV4UiXyKubOcEW0zWgxOJVVGsz5p69l9.C2TU5t.
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
//从 Spring Security 过滤器链中移除 CsrfFilter
.csrf().disable();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
06、Spring Security新的加密方法逐步替代原有的加密方法
内部源码:PasswordEncoderFactories
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);
}
SecurityConfig
1.如果我们没有手动配置密码加密工具,那么默认PasswordEncoder,实际上是DelegatingPasswordEncoder
2.DelegatingPasswordEncoder 代理了所有密码加密器,他会根据密码的字符串格式,而选择一个合适的加密工具
3.例如数据库前缀{noop}
/** * 如果我们没有手动配置密码加密工具,那么默认PasswordEncoder,实际上是DelegatingPasswordEncoder * * DelegatingPasswordEncoder 代理了所有密码加密器,他会根据密码的字符串格式,而选择一个合适的加密工具 * * noop * * @return */ // @Bean // PasswordEncoder passwordEncoder() { // return new BCryptPasswordEncoder(); // }
数据库可以多种加密方案共存
如何实现数据库统一类型密码加密-升级不同类型的密码
UserService
1.密码升级,实现UserDetailsPasswordService 接口
2.重新方法: *当用户登录的时候,会去自动检查当前用户密码是不是bcrypt,如果不是,则会自动进行密码升级,那么就会触发该方法
3.在这个方法种更新密码
@Service public class UserService implements UserDetailsService, UserDetailsPasswordService { @Autowired UserMapper userMapper; /** * 根据用户名查询用户对象 * 当用户登录的时候,会自动调用到这个方法 * @param username 登录的时候,用户输入的用户名 * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.loadUserByUsername(username); if (user == null) { //说明用户名不存在,这个异常最终会被隐藏起来,转而抛出 BadCredentialsException throw new UsernameNotFoundException("用户不存在"); } return user; }
/** * 当用户登录的时候,回去自动检查当前用户密码是不是bcrypt,如果不是,则会自动进行密码升级,那么 * 就会触发该方法,在这个方法种,更新密码即可 * @param user 当前用户密码 * @param newPassword 重新处理后的密码 * @return */
@Override public UserDetails updatePassword(UserDetails user, String newPassword) { Integer i = userMapper.updatePassWordByUsernam(newPassword,user.getUsername()); if (i == 1){ User user1 = (User) user; user1.setPassword(newPassword); return user1; } return user; }
UserService
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
Integer updatePassWordByUsernam(@Param("password") String newPassword, @Param("username")String username);
}
UserMapper.xml
<mapper namespace="com.qfedu.security02.mapper.UserMapper">
<update id="updatePassWordByUsername">
update user set password = #{password} where username;
</update>
<select id="loadUserByUsername" resultType="com.qfedu.security02.model.User">
select * from user where username=#{username};
</select>
</mapper>
升级同等类型的密码
SecurityConfig
1.首先要有passwordEncoder 统一的密码加密类型
2.重写PasswordEncoderFactories加入@Bean改变密码强度
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder(20));
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);
}
07、权限控制(核心功能)
1.HelloController:注明权限接口
-
设置可以访问的角色@PreAuthorize("hasRole('ROLE_user')")
@RestController public class HelloController { /** * 只要登录就能访问 * * @return */ @GetMapping("/hello") public String hello() { return "hello"; } /** * 具备 user 角色或者 admin 角色,就能访问 * * @return */ @GetMapping("/user/hello") @PreAuthorize("hasRole('ROLE_user')") public String user() { return "hello user"; } /** * 具备 admin 角色才能访问 * * @return */ @GetMapping("/admin/hello") @PreAuthorize("hasRole('ROLE_admin')") public String admin() { return "hello admin"; }
2.SecurityConfig:设置权限
-
.antMatchers("/admin/**").hasRole("admin"),角色会自动加上ROLE_admin数据库自动要保持一致
-
.antMatchers("/user/**").hasAnyRole("admin","user")
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasAnyRole("admin","user")
.anyRequest().authenticated()
.and()
//从 Spring Security 过滤器链中移除 CsrfFilter
.csrf().disable();
}
3.Role:角色实体类
package com.qfedu.security02.model; public class Role { private Integer id; private String name; 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; } }
4.user:在这里有获取用户角色/权限的方法getAuthorities()
public class User implements UserDetails { private Integer id; private String username; private String nickname; private String password; private Boolean enabled; private Integer role; // 一个用户可能有不同的角色 private List<Role> roles; public Boolean getEnabled() { return enabled; } public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public void setUsername(String username) { this.username = username; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public void setPassword(String password) { this.password = password; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public Integer getRole() { return role; } public void setRole(Integer role) { this.role = role; } /** * 返回用户的 角色/权限 * @return */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { // SimpleGrantedAuthority是他唯一的实现类 List<SimpleGrantedAuthority> list = new ArrayList<>(); return list; // return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList()); } /** * 获取用户的密码 * @return */ @Override public String getPassword() { return password; } /** * 获取用户名 * @return */ @Override public String getUsername() { return username; } /** * 账户是否没有过期 * @return */ @Override public boolean isAccountNonExpired() { return true; } /** * 账户是否没有被锁定 * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 密码是否没有过期 * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 账户是否可用 * @return */ @Override public boolean isEnabled() { return enabled; } }
5.UserService:在登录的成功的时候拿到用户角色
@Autowired
UserMapper userMapper;
/**
* 根据用户名查询用户对象
* 当用户登录的时候,会自动调用到这个方法
* @param username 登录的时候,用户输入的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
//说明用户名不存在,这个异常最终会被隐藏起来,转而抛出 BadCredentialsException
throw new UsernameNotFoundException("用户不存在");
}
//如过查询到了用户,接下来就去查询用户的角色
user.setRoles(userMapper.getUserRolesByUid(user.getId()));
return user;
}
UserMapper
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
Integer updatePassWordByUsername(@Param("password") String newPassword, @Param("username")String username);
List<Role> getUserRolesByUid(Integer uid);
}