在上一篇当中我们自定义了认证的登录页面,定了一个基本的表格,自定义了登录的密码,但是咱们都知道不能让所有用户都使用一个密码啊...
一、自定义认证登录页面细节分析
1.1 默认登录页面html页面详情
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" integrity="sha384-oOE/3m0LUMPub4kaC09mrdEhIc+e3exm4xOGxAmuFXhBNF4hcg/6MiAXAf5p0P56" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</p>
<input name="_csrf" type="hidden" value="WUrEFQvTxRrns-H-eiOdlycETF2qYMfVJ4ww3X5ccJWZmU97Py72JGrl9S_Kh9ScHg6poRA0YWWbA_f4FrlT7xo_E6P8oCxJ" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
</body></html>
以上是生成的默认的登录页面html源码,我们只看核心部分
html
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</p>
<input name="_csrf" type="hidden" value="WUrEFQvTxRrns-H-eiOdlycETF2qYMfVJ4ww3X5ccJWZmU97Py72JGrl9S_Kh9ScHg6poRA0YWWbA_f4FrlT7xo_E6P8oCxJ" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
这里需要注意的是:
- 请求方式必须是post
- 用户名称的参数默认必须是: username
- 密码的参数默认必须是: password
我们知道,所有的认证的时候,会执行UsernamePasswordAuthenticationFilter过滤器, 其中在UsernamePasswordAuthenticationFilter#attemptAuthentication方法当中
java
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// 判断一下当前的认证请求是不是post,不是就抛出异常
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 从请求参数当中取出用户名称
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
// 从请求参数当中取出密码
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
String username = obtainUsername(request);
String password = obtainPassword(request);
java
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
找到this.usernameParameter和this.passwordParameter
java
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
java
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
可以发现,这两上参数默认就是: username和passwork, 如果想要修改,则可以在配置类当中进行修改.
默认的UsernamePasswordAuthenticationFilter处理请求就是/login,在类当中定义了变量
java
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");
调用父类AbstractAuthenticationProcessingFilter构造方法的时候, 将其传递给父类了
java
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
// 给父类的成员变量赋值
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
判断当前登录的请求是不是/login
java
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
该方法,在doFilter里进行调用,对登录请求进行拦截,如果不是/login,则直接跳过当前的过滤器
java
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断一下是不是当前过滤器能处理的请求,如果不是,则跳过当前过滤器的执行,继续下一个过滤器的执行
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
// 略
如果要修改请求的uri,则可以在配置类当中进行配置
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return
http.authorizeHttpRequests(authorize -> {
// 如果请求的是/login.html则放行
authorize.requestMatchers("/login.html").permitAll()
.anyRequest().authenticated(); // 其它的请求都需要认证
}).formLogin(form -> { // 配置登录相关的信息
// 用来指定默认的登录页面, 注意: 一旦自定义登录页面以后必须配置一下登录的url【必须】
form.loginPage("/login.html")
// 自定义参数名称为yonghuming和mima
.usernameParameter("yonghuming")
.passwordParameter("mima")
// 自定义登录的url
.loginProcessingUrl("/dologin"); // ①. 指定处理登录的url【必须的】
}).csrf(AbstractHttpConfigurer::disable)
.build();
}
认证成功
1.2 FormLoginConfigurer
在自定义配置类当中,FormLoginConfigurer是一个用于配置表单登录功能的类.在配置类当中,主要使用的就是它
java
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
return HttpSecurity.this;
}
java
public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
}
其核心方法如下所示
- loginPage(String loginPage):
用于设置自定义的登录页面 URL。如果不设置,Spring Security 会使用默认的登录页面。 - loginProcessingUrl(String loginProcessingUrl):
指定登录处理的 URL。当用户提交登录表单时,请求会被发送到这个 URL 进行处理。 - usernameParameter(String usernameParameter)和passwordParameter(String passwordParameter):
分别用于设置用户名和密码在登录表单中的参数名称。默认情况下,用户名字段为 "username",密码字段为 "password"。 - defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse):
设置登录成功后的默认重定向 URL。alwaysUse参数决定是否总是使用这个 URL 进行重定向,即使之前有保存的请求(如在登录前试图访问一个受保护的资源)。 - failureUrl(String failureUrl):
指定登录失败后的重定向 URL。 - permitAll():
配置登录页面和登录处理 URL 可以被所有用户访问,即使没有经过身份验证。 - and():
用于连接配置,返回父类HttpSecurity对象以便继续进行其他安全配置。
以上可以根据自己的需求,进行选择性的进行配置.
二、自定义数据源
2.1 自定义数据源分析
当前我们将用户名称和密码配置在了application.yml文件当中了,但是不能所有用户都使用一个账号和密码啊啊啊啊.再者,先前所有的账号密码都存储在了内存当中,应用重新就啥也没有了,以我们的经验来说,用户账号信息等数据肯定需要持久化存储的.
根据上一篇当中认证流程分析的流程图解
回顾一下这里的核心逻辑:
java
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
this.getUserDetailsService()返回的是UserDetailsService接口,实际上这里返回的是InMemoryUserDetailsManager对象,因为在程序启动的时候, 已经将InMemoryUserDetailsManager放到容器里了.
在UserDetailsServiceAutoConfiguration配置类当中, 可以看到该生效的条件当中
java
@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@Conditional(MissingAlternativeOrUserPropertiesConfigured.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder")
public class UserDetailsServiceAutoConfiguration {
}
其中我们看
java
@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
AuthenticationManagerResolver.class }
@ConditionalOnMissingBean表示如果在容器当中, 没有AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,AuthenticationManagerResolver.class几个对象,则配置类生效, 反之,如果我们要自己定义的数据源, 不再采用默认的基于内存存储,则可以直接将它们其中的任意一个对象,放到容器里去即可.这里我们根据业务选择【自己去实现UserDetailsService即可,重写抽象方法】.
2.2 参考示例
在项目当中,直接定义UserDetailsService接口的实现类即可,这里我们先模拟一下操作即可
java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 模拟数据库中查询到的用户名称和密码
String name = "rj";
String password = "rj";
// 判断一下用户名称和密码是否正确
if(!name.equals(username)){
throw new UsernameNotFoundException("用户名或密码错误");
}
// 如果用户名称正确,则去数据源当中,根据用户名称取出相应的用户数据.
// 这里模拟一下操作,即可.后续再添加上数据源,我们这里暂定使用MySQL
// 将取出的用户数据,封装成 UserDetails 对象返回即可
// 结合之前的分析,User是 UserDetails 的子类,所以直接返回即可
// 参数说明
// 1. 用户名
// 2. 密码
// 3. 角色、权限列表
return new User(username, password, Collections.emptyList());
}
}
自定义数据源就是这么简单.
- 注意事项
- 示例代码仅仅是模拟操作
- User不要导错包,它org.springframework.security.core.userdetails包下,切记.
- User的构造方法,最后一个表示角色、权限列表目前我们没有步及,这里传入了一个空的集合
2.3 验证
启动服务, 访问/hello接口,输入我默认的用户名称和密码,点击提交
到此,自定义数据源完成,总体来说,理解了整个认证流程,理解起来不是很难.
三、总结
3.1 重点总结
- 对于认证登录细节分析
- 自定义数据源,即UserDetailsService, 其它的【核心业务】依然是由SpringSecurity帮助我们完成的, 如下图所示,我们只是开了个头,然后提供了一下认证数据而已
- 以上的自定义认证的登录页面、数据源只适用于前后端不分离的情况下使用
3.2 下篇内容
- 前后端分离的情况下自定义认证流程