1.为何要做自定义登录页面以及校验
在项目中配置了spring-security的模块的项目中,spring boot会默认帮我们生成的一个简洁的登录页面,它会在我们访问任何请求的时候弹出来
用户名是默认的:user,密码是需要我们找到我们启动项目时的日志,里面会随机生成一个默认的密码
输入之后就可以访问到我们的资源信息了
但这种场景给我们的项目场景肯定大相径庭了,不管是登录的页面还是校验的规则在实际的项目中都是比较复杂的。
这个时候就需要我们自定义登录页面以及自定义校验了
2.如何自定义
需要继承WebSecurityConfigurerAdapter这个类,并重写configure方法
2.1 一个简单的基于表单登录
但是configure有三个方法(从源码可知),分别接收不同的参数,我们应该重写哪一个呢?
回到我们原来的设想,是想让程序使用表单进行登录
首先配置一个最简单的,基于表单认证的一个页面配置
这个时候我们需要覆盖configure(HttpSecurity http)方法
protected void configure(HttpSecurity http) throws Exception {
//用表单登录,进行身份认证,所有的请求都需要进行身份认证才可以访问
http.formLogin() //表单登录的意思(指定了身份认证的方式)
//授权
.and()
//对请求进行一个授权
.authorizeRequests()
.anyRequest().authenticated();//任何请求都需要身份认证
}
这个时候 启动项目,再次访问请求,会看到一个表单页面(也是spring security提供给我们的)
用户名和密码还是上面一样,user和控制台输出的密码(每次启动密码都会变)
这个时候观察浏览器上的地址变化会和http.httpBasic()这种方式不太一样,我们访问的地址是
http://localhost:8080/user/2 在访问的时候会帮我们强制跳转至:http://localhost:8080/login 页面(登录页面),在认证成功之后会自动帮我们重定向xx/user/2 这个链接。
2.2Spring Security核心原理(过滤器链)
所有访问服务的请求都会经过spring security它的过滤器,响应也同样会。这些过滤器在项目启动的时候spring boot会自动配进去。
作用:用来认证用户的身份,每个过滤器负责一种认证方式
对于刚才我们的登录而言:对于表单登录的由UsernamePasswordAuthenticationFilter,对于基本登录的(也就是http.httpBasic)则由BasicAuthenticationFilter来处理。
例如:对于表单登录它会怎么样来判断这个请求会走这个Filter?
对于Filter来说,它会检查当前的请求中是否有这个Filter所需要的信息,对于UsernamePasswordAuthenticationFilter来说,首先这个请求是否是登录请求,请求中带没带用户名(username)和密码(password),如果带了,这会尝试用这个账户名和密码进行登录,如果没有带,则会放行,走到下一个Filter中。
任何一个Filter成功完成了用户登录以后会在请求上做一个标记(这个用户认证成功了)
最终会到一个FilterSecurityInterceptor过滤器中,是该过滤器链的最后一环。
在这个过滤器中它会决定你当前的请求能不能去访问后面的Controller,依据什么来判断呢?依据我们configure方法中所配置的。
我们现在的配置是:所有的请求都需要经过身份认证才能访问,此时这个Filter会判断当前的请求经过了前面的某一个Filter的身份认证。
针对复杂场景:针对某些请求,只能有VIP用户才能访问,这些规则都会被放在这个FilterSecurityInterceptor里面,这个过滤器会根据这些规则做判断,判断的结果是过还是不过,不过的话,会根据不同的原因抛出不同的异常。比如 如果没有经过身份认证,则抛出一个没身份认证的一个异常;如果只有VIP才能访问这个请求,那么也需要抛出异常,因为权限不够。
在异常抛出去之后,会有一个异常过滤器来拦截ExceptionTranslationFilter,这个异常过滤器就是用来捕获FilterSecurityInterceptor里面所抛出来的异常,它会根据里面抛出来的异常做响应的处理,如果没有没有登录则引导用户去登录等。
在过滤器链中,除了绿色的过滤器(UsernamePasswordAuthenticationFilter/BasicAuthenticationFilter/...)是可以通过配置来禁用或者启用的,对于其他颜色的过滤器都是不能控制的,一定会在过滤器链上,而且位置是不可变的。
2.3通过调试解析spring security登录的一个过程
我们需要在四个类里面打上断点
1.第一个断点(Controller层)
2.第二个断点:(FilterSecurityInterceptor层)
3.第三个断点:(ExceptionTranslationFilter层)
4.第四个断点:(UsernamePasswordAuthenticationFilter层)
从UsernamePasswordAuthenticationFilter这个过滤器中,它只会处理/login post的请求
收到请求只会,它会从request中拿到用户名和密码,然后做登录。
发送请求,因为请求的参数中没有username/password参数,直接就到了最后一个过滤器(FilterSecurityInterceptor)由它来判断是否被拦截。
因为我们配置了所有请求都需要进行身份认证,断点进行下一步时,则会抛出来一个异常(ExceptionTranslationFilter)
捕获到的是未授权异常。这个时候前台页面就会回到了登录页面
在登录页面输入完,正确的账户名和密码之后,断点会来到
UsernamePasswordAuthenticationFilter
它会从request中取到用户名和密码进行校验,校验成功后会继续回到了
(FilterSecurityInterceptor)中的super.beforeInvocation(fi)方法,为什么呢?因为之前的请求是
http://localhost:8080/user/2 它是这个资源访问(Controller),在登录成功之后会有个资源的跳转。
在doFilter之后就调到我们自己的Controller中的内容了。
全部完成之后,在页面的前台就可以看到请求的信息了。
2.4自定义登录账户校验
为什么需要自定义呢,因为这个我们的业务场景不仅仅在是个简单账户的校验,需要根据我们自己的业务逻辑来实现相关的功能。
相关功能点:
用户信息的获取逻辑在spring security中是被封装在一个接口中(UserDetailsService)
在此接口中只有一个loadUserByUsername方法
根据用户在前台传输过来的用户名来查询用户信息,用户的信息被封装在UserDetails的实现类里面,这个实现类返回以后,做一下响应的处理,校验都通过了 就会把这个用户放到Session里面,就会认为你的登录成功了。
找不到用户则会抛异常UsernameNotFoundException,也会被响应的处理类接收。
实现代码
- 处理用户信息的获取(从数据库查询)
@Component
public class MyUserDetailsService implements UserDetailsService{
private static final Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);
//@Autowired
//private XXService xxService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("根据用户名查询用户信息"+username);
//查询用户信息
//xxService.query(username);
return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
拿到用户信息之后需要组装成UserDetails对象,但是它本身就是一个接口 应该怎么返回呢?
这个时候需要用到spring security中给我们提供的一个User对象,这个对象已经实现了UserDetails接口
我们需要用到它的方法
第一个参数就是用户名,第二个参数就是密码,第三个参数就是我们的角色(做授权)。
此时我们就可以在浏览器上访问我们的请求地址了:http://localhost:8080/login
账号随便,密码为123456,输入错误的密码则会被抛出来异常
密码正确,则被正常放行。
- 处理用户校验逻辑(是否冻结,是否过期等)
常规业务情况需要返回UserDetails的实现类去实现用户的锁定,过期,注销等操作,这些方法是UserDetails中帮我们定义的,也可以使用User实现类。
public class MyUser implements UserDetails {
private String username;
private String password;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
public MyUser(String username, String password, boolean accountNonExpired, boolean accountNonLocked,
boolean credentialsNonExpired, boolean enabled) {
this.username = username;
this.password = password;
this.accountNonExpired = accountNonExpired;
this.accountNonLocked = accountNonLocked;
this.credentialsNonExpired = credentialsNonExpired;
this.enabled = enabled;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
}
在处理失效的属性时,置为无效;
return new MyUser(username, "123123", true, true, true, false);
再次启动,登录的用户信息都会是已经失效的用户
-
处理密码加密解密
处理加密和解密是一个新的接口(PasswordEncoder)。
是org.springframework.security.crypto.password包下面的类。
加密方法是我们自己调用的(encode),而解密方法是spring security是自动调用的,根据UserDetails的实现类中的密码是自动调用的。
配置加密在BrowserSecurityConfig类中配置密码加密
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
BCryptPasswordEncoder是spring security推荐的一个加密方法,也可以实现自己的加密方法,只需要实现PasswordEncoder接口的实现类即可。
这个时候我们的登录接口则需要修改为:
//常规情况下,passwordEncoder.encode("123123")是注册时需要做的方法,
//而登录的时候则只需要传递password即可
return new MyUser(username, passwordEncoder.encode("123123"), true, true, true, false);
为了更好的观察到 BCryptPasswordEncoder的强大之处,打印出账号的密码。
String password = passwordEncoder.encode("123123");
logger.info(username+"的密码是:"+password);
此时我们启动服务,输入我们的请求地址。
登录成功之后可以看到后台会输出 BCryptPasswordEncoder加密后的密码:
当我们使用同一账户进行反复登录时继续观察日志
会发现,两次的加密后的密码是不一致的。
原理
同样的一个密码,随机生成一个盐值,并且在最后生成密码串的时候把随机生成的盐混到这个串里面,每次判断的是可以用随机生成的盐+生成的串去比对,最终来判断密码是否匹配。这样就可以避免同一个密码被反复盗用。
转载于:https://blog.51cto.com/mazongfei/2334112