使用SpringSecurity认证流程
在pom.xml文件中入依赖
导入依赖后运行服务器,通过服务器的端口号可以直接访问SpringSecurity框架默认的登录界面,默认的用户名为user,密码会随机生成在服务端的控制台上;所有的请求都需要登录;没有登录默认跳转登陆页面;登录后所有的post请求都是不可用的。
<!--Spring Boot中的Spring Security的依赖项,用于处理认证与授权-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
登陆后post请求不可用,为了防止伪造跨域攻击的保护
自定义SpringSecurity配置
自定义配置文件的目的是为了不输入帐号密码就可访问一些页面,就需要自定义配置文件去配置一些白名单以及配置一些请求授权。以下安全配置之后,不在白名单的请求都不能访问,除非登录
@EnableWebSecurity(debug = true) // 开启调试模式,在控制台将显示很多日志,在生产环境中不宜开启
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 白名单
String[] urls = {
"/favicon.ico",
"/doc.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources",
"/v2/api-docs",
"/resources/**", // 静态资源文件夹,通常是上传的文件,请与配置文件中的"tea-store.upload.base-dir-name"一致
"/account/users/login" // 用户登录
};
// 配置请求授权
// 如果某个请求被多次配置,按照“第一匹配原则”处理
// 应该将精确的配置写在前面,将较模糊的配置写在后面
http.authorizeRequests()
// .mvcMatchers(HttpMethod.OPTIONS, "/**") // 匹配所有OPTIONS类型的请求
// .permitAll() // 许可
.mvcMatchers(urls) // 匹配某些请求
.permitAll() // 许可,即不需要通过认证就可以访问
.anyRequest() // 任何请求,从执行效果来看,也可以视为:除了以上配置过的以外的其它请求
.authenticated(); // 需要通过认证才可以访问
// 是否调用以下方法,将决定是否启用默认的登录页面
// 当未通过认证时,如果有登录页,则自动跳转到登录,如果没有登录页,则直接响应403
//无此方法,没在白名单的请求是403
http.formLogin();
}
}
禁用跨域攻击防御机制
禁用"防止伪造的跨域攻击的防御机制",否则所有post请求将失效(后期自己解决跨域攻击问题)
//禁用"防止伪造的跨域攻击的防御机制"
http.csrf().disable();
SpringSecurity框架为了应对跨域攻击行为,要求post请求必须要提交一个服务器给出的随机值。在使用禁用跨域攻击前,SpringSecurity默认的登录界面的html的代码中会默认生成一个UUID的编号,就是防止跨域恶意攻击.可以通过在登录界面右键检查源代码就可以看到下面代码的具体位置.若在自定义配置文件添加了上面一句代码,进行禁用,下面代码在登录页面html代码中就会不存在.
<input name="_csrf" type="hidden" value="f6979fc0-b4a7-4dc0-bf9c-9781b6b1419b" />
UUID:唯一随机数。它的构成是32位英文以及数字加上四个减号,出去减号,他是一个32位长的16进制数,每一位相当于二进制的4位,相当于出去减号是一个128位的2进制数,穷举所有结果需要2的128次方次。
密码加密
在自定义SpringSecurity配置文件中加入密码加密配置,将非加密密码转化为加密的密码存入数据库。使用BCrypt算法使用密码加密。
@Bean
public PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
BCrypt算法
原文相同,密文不同;处理速度很慢
public class BCryptTests {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(15);//参数处理速度变慢
@Test
void md5Matches() {
String rawPassword = "123456";
String testPassword = "59718f39-9647-4b02-9c3f-dea33e3633f98ea1dfcb56b09cce2e356125687e3f8f";
String salt = testPassword.substring(0, 36);
String encodedPassword = DigestUtils.md5DigestAsHex((salt + rawPassword).getBytes());
System.out.println(testPassword.equals(salt + encodedPassword));
}
@Test
void bcryptEncode() {
String rawPassword = "123456";
long start = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
passwordEncoder.encode(rawPassword);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
重写UserDetails
在真正的开发中我们是不会用到SpringSecurity框架的默认的用户名密码,还有默认的登录界面。首先在配置类中注释掉默认的登录方法http.formLogin();
// 是否调用以下方法,将决定是否启用默认的登录页面
// 当未通过认证时,如果有登录页,则自动跳转到登录,如果没有登录页,则直接响应403
//无此方法,没在白名单的请求是403
//http.formLogin();
要想使用自己的用户名密码,首先要拿到UserDetailsService类型的对象,新建一个类去实现UserDetailsService接口,重写loadUserByUsername。
执行原理:通过用户输入用户名,来调用该参数所在的方法(loadUserByUsername),返回一个UserDetails对象里会包含密码,框架自动判断密码。加了这个组件类之后随机密码就没了,判断一个用户帐号能不能登录,就以此代码逻辑为准。
UserDetails是一个接口,SpringSecurity框架给它提供了一个典型实现类User ,builder():构建器,build():构建。
/**
* Spring Security处理认证时使用到的获取用户登录详情的实现类
*/
@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if ("root".equals(s)) {
UserDetails userDetails = User.builder()
.username("root")
.password("123456")
.disabled(false) // 账号是否被禁用
.accountLocked(false) // 账号是否被锁定,当前项目中无此概念,则所有账号的此属性都是false
.accountExpired(false) // 账号是否过期,当前项目中无此概念,则所有账号的此属性都是false
.credentialsExpired(false) // 凭证是否过期,当前项目中无此概念,则所有账号的此属性都是false
.authorities("山寨权限")
.build();
return userDetails;
} else {
return null;
}
}
}
从数据库查数据进行登录认证
XML文件中SQL语句:
@Data
public class UserLoginInfoVO implements Serializable {
private Long id;
private String username;
private String password;
private String nickname;
private String avatar;
private Integer enable;
}
<!-- UserLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultType="server.account.UserLonginInfoVO">
SELECT
id,username,password,nickname,avatar,enable
FROM
account_user
WHERE
username=#{username}
</select>
当帐号被禁用和密码错误时,SpringSecurity框架先判断帐号是否被禁用,在判断密码是否错误。
//Spring Security处理认证时使用到的获取用户登录详情的实现类
@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("Spring Security框架自动调用了UserDetailsService对象,将根据用户名获取用户详情,参数:{}", s);
UserLoginInfoVO loginInfo = userRepository.getLoginInfoByUsername(s);
log.debug("根据用户名【{}】从数据库中查询用户详情,查询结果:{}", s, loginInfo);
if (loginInfo == null) {
return null;//回头报错,全局异常捕获
}
UserDetails userDetails = User.builder()
.username(loginInfo.getUsername())
.password(loginInfo.getPassword())
.disabled(loginInfo.getEnable() == 0) // 账号是否被禁用
.accountLocked(false) // 账号是否被锁定,当前项目中无此概念,则所有账号的此属性都是false
.accountExpired(false) // 账号是否过期,当前项目中无此概念,则所有账号的此属性都是false
.credentialsExpired(false) // 凭证是否过期,当前项目中无此概念,则所有账号的此属性都是false
.authorities("山寨权限")
.build();
log.debug("即将向Spring Security框架返回UserDetails类型的结果:{}", userDetails);
log.debug("接下来,将由Spring Security框架自动验证用户状态、密码等,以判断是否可以成功登录!");
return userDetails;
}
}
AuthenticationManager登录验证
首先在白名单中加入登录的访问路径
param类
/**
* 用户登录的参数类
*/
@Data
public class UserLoginInfoParam implements Serializable {
//用户名
@NotNull(message = "请提交用户名")
@Pattern(regexp = "^[a-zA-Z0-9]{4,15}$", message = "用户名必须是4~15长度的字符组成,且不允许使用标点符号")
@ApiModelProperty(value = "用户名", required = true, example = "root")
private String username;
// 密码(原文)
@NotNull(message = "请提交密码")
@Pattern(regexp = "^.{4,15}$", message = "密码必须是4~15长度的字符组成")
@ApiModelProperty(value = "密码", required = true, example = "123456")
private String password;
}
service层接口设计
//用户登录
void login(UserLoginInfoParam userLoginInfoParam);
在SecurityConfiguration配置类中添加一个方法,登录的具体实现要用到认证管理器接口AuthenticationManager,也可以使用authenticationManager()方法,但是执行测试时有可能造成死循环。
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
判断账号能否登录:
在身份验证管理器(AuthenticationManager)方法上加以@Bean注解,将其返回的认证管理器对象,交由spring容器管理,在登陆的service层接口实现类要用到认证管理器,将其注入到测类中,拿到认证管理器,调用authenticate()有参方法进行身份验证,需要一个身份验证信息Authentication接口类型的参数对象,通过认证信息Authentication的实现类UsernamePasswordAuthenticationToken进行传入需要认证的信息(例如:帐号、密码等)。在这完成之后,用户登陆成功,还是不能访问其他功能,访问返回403。