在上一篇文章的结尾,我们列入了默认使用 SpringSecurity 一些待优化和解决的问题,我们再来回顾一下
- 用户登录不可能以这种弹框形式去登录,一般网页都有自己的登录页面(自定义登录页面)
- 用户名、密码应该是从数据库中读取,而不是默认和随机的(自定义认证逻辑)
- 并不是对所有的资源或接口都需要认证(设置资源白名单)
- 认证成功或者失败的处理,比如登录成功可以做一些记录,失败做一些处理
本篇文章就主要解决上面四点问题
自定义登录页面/登录地址
OK,首先第一点,让我们来解决一下,将默认的弹框登录方式改为网页表单登录方式。我们只需要在我们的项目中自定义一个 WebSecurityConfigurerAdapter 的实现类,并重写它的 configure(HttpSecurity http) 方法,在这个方法中我们显示指定登录方式为 formLogin (默认为 httpBasic ) 示例如下:
1 | /** |
然后,重新启动应用,再次访问 http://localhost:8080/security/hello 接口

用户名任然是 user,密码是日志中输出的 password。如果我们输错了用户名、密码,会有如下提示

输入正确则可以访问到我们的接口资源。
OK,到目前为止我们将认证方式从 HttpBasic 转变为了 FormLogin 登录,但是还是离我们的要求有一些差距
- 登录页面虽然是表单登录了,但是是默认的。我们需要自定义的登录页面。
- 在前后端分离的情况下,我们需要自定义登录接口地址,给 App 端使用。
我们此时分析一下,发现问题的核心不在于登录页面,也不在于登录接口。我们上面访问一个资源跳转到所谓的登录页面。实质上是系统判断我们没有认证,引导我们跳转到一个地址,这个地址既可以是一个 web 页面,也可以是一个 restful 接口。所以上面两个问题本质上是一个问题,就是配置系统的表单认证地址。具体实例如下:
1 | /** |
这里我们注意,loginPage 指定认证页面地址,loginProcessingUrl 指定认证地址,两者只需要配置一个即可,如果都配置了,则只有 loginProcessingUrl 生效。
这里我们可能会遇到一个需求,我们的后端应用同时给 web 页面与 app 提供服务,这样他们的认证引导方式不一样,该怎么解决。我们要注意的是我们可以在 loginProcessingUrl 配置的接口里通过对请求的判断来动态对 web 和 app 请求进行定制化处理。
另外,如果配置的是 loginPage,则需要设置 .antMatchers("/page/login.html").permitAll() 表示认证页面的访问不需要认证,否则会死循环导致重定向次数过多问题。这样我们就完美的解决了自定义登录页面与地址问题,第一个问题解决。
设置资源白名单
既然这里用到了 antMatcher 与 permitAll,那我们提前说一下第三个问题,资源白名单,这里要分情况讨论一下:
- 前后端分离
- 前端页面资源不在我们后端应用的管辖下,我们只需要管理好我们的接口访问权限即可。
- 不分离
- 前端页面放在应用文件夹下,那么就需要对对应的文件路径进行管理。
具体管理方式有两种,一种指定具体的访问地址,例如 .antMatchers("/page/login.html").permitAll() ,这里还可以使用 * 通配符进行范围指定
-
/page/*.html: page 下的所有 html -
/page/**: page 下的所有资源
另一种方式是在自定义的 WebSecurityConfig 类中重写 configure(WebSecurity web) 方法,在方法中对静态资源设置不拦截,这里注意一下,spring boot 的默认静态资源放置位置是在 resource/static 下,可以在 static 下新建一个文件夹,然后在上述方法中指定跳过拦截的文件路径即可。
1 |
|
到了这里,第三个问题基本上也解决了。那我们还剩下两个问题要处理,自定义认证逻辑与认证结果处理。我们按照业务顺序先来处理一下自定义认证逻辑。
自定义认证逻辑
如果让我们来设计认证流程,自定义认证逻辑我们可以分为三块
- 从请求中获取用户认证信息,在表单认证这里就是用户名与密码
- 按照认证信息从数据库查询取出用户信息
- 对取出的用户信息与认证信息进行校验比对
- 比对密码
- 校验用户状态,比如账号是否是冻结的等等
如果使用 SpringSecurity 默认帮我们实现的表单认证逻辑,我们只需要实现第二步即可,具体步骤如下:
-
自定义一个
UserDetailsService的实现类,重写它的loadUserByUsername方法,在这个方法里面按参数到数据库中查询用户信息,最后返回一个UserDetail的实现类。示例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* @author: hblolj
* @Date: 2019/3/14 10:40
* @Description:
* @Version:
**/
public class FormUserDetailService implements UserDetailsService{
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// TODO: 2019/3/14 按参数 s 从数据库查找用户信息,一般注入 dao 查询
// 返回的是 org.springframework.security.core.userdetails 下 User 类
// 在实际业务时,可以使系统的 User 类去实现 UserDetail 接口,然后返回自己的 User 类即可
// 构造方法传入的三个参数分别是用户名、密码、权限集合
// 还有另外一个构造方法,可以传额外第四个参数,表示账号状态(启用、冻结、锁定等)
// 如果密码使用了加密,从数据库中取出的应该是加密过的密码,不是明文
return new User(s, "123", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}这样,当我们使用表单登录时就会使用我们自定义的逻辑了(默认使用的其实是
InMemoryUserDetailsManager这个类)。 -
这里有几点注意说明一下
-
在用户注册时对用户密码使用了加密时的处理。
-
SpringSecurity给我们提供了PasswordEncoder来加密密码,我们可以指定一种加密类型,然后放入IOC容器中,加密解密使用这个共享的PasswordEncoder。1
2
3
4
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}我们注册时,可以使用该
PasswordEncoder对用户的密码进行加密存储到数据库中,取出时,SpringSecurity会从获取到该passwordEncoder来进行解密校验。我们自己模拟的时候,可以对密码进行加密返回。示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* @author: hblolj
* @Date: 2019/3/14 10:40
* @Description:
* @Version:
**/
public class FormUserDetailService implements UserDetailsService{
private PasswordEncoder passwordEncoder;
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 模拟从数据库中取出的密码是已经加密过的密码
String password = passwordEncoder.encode("123");
User user = new User(s, password, true, true, true,
true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
return user;
}
}
-
-
当自己定义了多个
UserDetailsService的实现类放到IOC容器时,会发现默认的formLogin会使用InMemoryUserDetailsManager的实现来处理校验逻辑。同时SpringScurity使用的PasswordEncoder也不是我们自己实现的,会出现密码校验不上-
解决方案,全局指定默认的
UserDetailService与PasswordEncoder1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
* @author: hblolj
* @Date: 2019/3/15 18:12
* @Description: 指定全局默认的 UserDetailService 与 PasswordEncoder
* @Version:
**/
public class GlobalAuthenticationConfigurer extends GlobalAuthenticationConfigurerAdapter {
private final UserDetailsService userService;
private final PasswordEncoder passwordEncoder;
public GlobalAuthenticationConfigurer(@Qualifier("formUserDetailService") UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.userService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
}
-
-
系统提供的
formLogin不能满足我们的需求,需要自定义认证方式,比如短信验证码登录、微信登录等等。- 下一章节会示例,To Be Continue…
-
认证结果自定义处理
经过前面的认证,现在会有两个结果,认证成功与认证失败。
我们需求往往会要求我们正在这时做出对应的处理,比如记录信息、引导用户,返回用户信息等等。
在 SpringSecurity 里面,框架帮我们封装了两个接口 ( AuthenticationFailureHandler 与 AuthenticationSuccessHandler ),我们只需要实现这两个接口,重写 ( onAuthenticationFailure 与 onAuthenticationSuccess 方法) 并将其实现类配置到我们自定义的 WebSecurityConfig 即可使用。
-
认证成功处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* @author: hblolj
* @Date: 2019/3/14 14:56
* @Description:
* @Version:
**/
4j
public class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
log.info("Login Success!");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(authentication.getPrincipal().toString());
}
} -
认证失败处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* @author: hblolj
* @Date: 2019/3/14 14:56
* @Description:
* @Version:
**/
4j
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler{
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
// 自定义登录失败处理逻辑
log.info("Login Failure!");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(e.getMessage());
}
} -
添加到配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/**
* @author: hblolj
* @Date: 2019/3/14 10:07
* @Description:
* @Version:
**/
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationSuccessHandler authenticationSuccessHandler;
private AuthenticationFailureHandler authenticationFailureHandler;
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 指定登录认证方式为表单登录
//指定自定义登录页面地址,一般前后端分离,这里就用不到了
// .loginPage("http://www.baidu.com")
// 自定义表单登录的 action 地址,默认是 /login
.loginProcessingUrl("/authentication/form")
// 设置认证成功处理器与认证失败处理器
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.authorizeRequests()
// 允许登录页面不需要认证就可以访问,不然会死循环导致重定向次数过多
.antMatchers("/page/login.html").permitAll()
.anyRequest() // 对所有的请求
.authenticated(); // 都进行认证
// .and()
// .exceptionHandling()
// .authenticationEntryPoint(authenticationEntryPoint); // 实现了 EntryPoint 对 loginPage 有覆盖作用,loginPage 不生效
}
}
这里要注意几点,在我们的需求中可能会出现,比如登录前访问 A 页面,现在登陆后需要自动跳转到 A 页面。这里我们可以观察一下,AuthenticationSuccessHandler 与 AuthenticationFailureHandler 接口的实现类
-
1
SavedRequestAwareAuthenticationSuccessHandler
- 继承该类,调用
super.onAuthenticationSuccess方法,会跳转到认证前的页面
1
SimpleUrlAuthenticationFailureHandler
- 继承该类,调用
super.onAuthenticationFailure方法会跳转到设置的页面,如果没有设置会返回 401,同时可以指定forward与redirect方式
- 继承该类,调用
关于适配 web 与 app 方面,在处理方法中从请求中分析出客户端类型,然后做出对应的处理即可。比如是引导页面跳转,还是返回一段 JSON。
OK,到了这里,开头我们的几个目标问题都已经解决了,下一篇文章我们将给大家带了在 SpringSecurity 下自定义认证方式的实现说明(手机号登陆)。
To Be Continue…