前面的关于 Spring Security 相关的文章只是一个预热。为了接下来更好的实战,若是你错过了请从 Spring Security 实战系列 开始。安全访问的第一步就是认证(Authentication
),认证的第一步就是登陆。今天咱们要经过对 Spring Security 的自定义,来设计一个可扩展,可伸缩的 form 登陆功能。html
下面是 form 登陆的基本流程:前端
只要是 form 登陆基本都能转化为上面的流程。接下来咱们看看 Spring Security 是如何处理的。java
昨天 Spring Security 实战干货:自定义配置类入口WebSecurityConfigurerAdapter 中已经讲到了咱们一般的自定义访问控制主要是经过 HttpSecurity
来构建的。默认它提供了三种登陆方式:web
formLogin()
普通表单登陆oauth2Login()
基于 OAuth2.0
认证/受权协议openidLogin()
基于 OpenID
身份认证规范以上三种方式通通是 AbstractAuthenticationFilterConfigurer
实现的,spring
启用表单登陆经过两种方式一种是经过 HttpSecurity
的 apply(C configurer)
方法本身构造一个 AbstractAuthenticationFilterConfigurer
的实现,这种是比较高级的玩法。 另外一种是咱们常见的使用 HttpSecurity
的 formLogin()
方法来自定义 FormLoginConfigurer
。咱们先搞一下比较常规的第二种。数据库
该类是 form 表单登陆的配置类。它提供了一些咱们经常使用的配置方法:json
loginPage(String loginPage)
: 登陆 页面而并非接口,对于先后分离模式须要咱们进行改造 默认为 /login
。loginProcessingUrl(String loginProcessingUrl)
实际表单向后台提交用户信息的 Action
,再由过滤器UsernamePasswordAuthenticationFilter
拦截处理,该 Action
其实不会处理任何逻辑。usernameParameter(String usernameParameter)
用来自定义用户参数名,默认 username
。passwordParameter(String passwordParameter)
用来自定义用户密码名,默认 password
failureUrl(String authenticationFailureUrl)
登陆失败后会重定向到此路径, 通常先后分离不会使用它。failureForwardUrl(String forwardUrl)
登陆失败会转发到此, 通常先后分离用到它。 可定义一个 Controller
(控制器)来处理返回值,可是要注意 RequestMethod
。defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse)
默认登录成功后跳转到此 ,若是 alwaysUse
为 true
只要进行认证流程并且成功,会一直跳转到此。通常推荐默认值 false
successForwardUrl(String forwardUrl)
效果等同于上面 defaultSuccessUrl
的 alwaysUse
为 true
可是要注意 RequestMethod
。successHandler(AuthenticationSuccessHandler successHandler)
自定义认证成功处理器,可替代上面全部的 success
方式failureHandler(AuthenticationFailureHandler authenticationFailureHandler)
自定义失败成功处理器,可替代上面全部的 failure
方式permitAll(boolean permitAll)
form 表单登陆是否放开知道了这些咱们就能来搞个定制化的登陆了。安全
接下来是咱们最激动人心的实战登陆操做。 有疑问的可认真阅读 Spring 实战 的一系列预热文章。app
咱们的接口访问都要经过认证,登录错误后返回错误信息(json),成功后前台能够获取到对应数据库用户信息(json)(实战中记得脱敏)。cors
咱们定义处理成功失败的控制器:
@RestController
@RequestMapping("/login")
public class LoginController {
@Resource
private SysUserService sysUserService;
/** * 登陆失败返回 401 以及提示信息. * * @return the rest */
@PostMapping("/failure")
public Rest loginFailure() {
return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登陆失败了,老哥");
}
/** * 登陆成功后拿到我的信息. * * @return the rest */
@PostMapping("/success")
public Rest loginSuccess() {
// 登陆成功后用户的认证信息 UserDetails会存在 安全上下文寄存器 SecurityContextHolder 中
User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = principal.getUsername();
SysUser sysUser = sysUserService.queryByUsername(username);
// 脱敏
sysUser.setEncodePassword("[PROTECT]");
return RestBody.okData(sysUser,"登陆成功");
}
}
复制代码
而后 咱们自定义配置覆写 void configure(HttpSecurity http)
方法进行以下配置(这里须要禁用crsf):
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/process")
.successForwardUrl("/login/success").
failureForwardUrl("/login/failure");
}
}
}
复制代码
使用 Postman 或者其它工具进行 Post 方式的表单提交 http://localhost:8080/process?username=Felordcn&password=12345
会返回用户信息:
{
"httpStatus": 200,
"data": {
"userId": 1,
"username": "Felordcn",
"encodePassword": "[PROTECT]",
"age": 18
},
"msg": "登陆成功",
"identifier": ""
}
复制代码
把密码修改成其它值再次请求认证失败后 :
{
"httpStatus": 401,
"data": null,
"msg": "登陆失败了,老哥",
"identifier": "-9999"
}
复制代码
就这么完了了么?如今登陆的花样繁多。常规的就有短信、邮箱、扫码 ,第三方是之后我要讲的不在今天范围以内。 如何应对想法多的产品经理? 咱们来搞一个可扩展各类姿式的登陆方式。咱们在上面 2. form 登陆的流程 中的 用户 和 断定 之间增长一个适配器来适配便可。 咱们知道这个所谓的 断定就是 UsernamePasswordAuthenticationFilter
。
咱们只须要保证 uri
为上面配置的/process
而且可以经过 getParameter(String name)
获取用户名和密码便可 。
我忽然以为能够模仿 DelegatingPasswordEncoder
的搞法, 维护一个注册表执行不一样的处理策略。固然咱们要实现一个 GenericFilterBean
在 UsernamePasswordAuthenticationFilter
以前执行。同时制定登陆的策略。
定义登陆方式枚举 ``。
public enum LoginTypeEnum {
/** * 原始登陆方式. */
FORM,
/** * Json 提交. */
JSON,
/** * 验证码. */
CAPTCHA
}
复制代码
定义前置处理器接口用来处理接收的各类特点的登陆参数 并处理具体的逻辑。这个借口其实有点随意 ,重要的是你要学会思路。我实现了一个 默认的 form' 表单登陆 和 经过
RequestBody放入
json` 的两种方式,篇幅限制这里就不展现了。具体的 DEMO 参见底部。
public interface LoginPostProcessor {
/** * 获取 登陆类型 * * @return the type */
LoginTypeEnum getLoginTypeEnum();
/** * 获取用户名 * * @param request the request * @return the string */
String obtainUsername(ServletRequest request);
/** * 获取密码 * * @param request the request * @return the string */
String obtainPassword(ServletRequest request);
}
复制代码
该过滤器维护了 LoginPostProcessor
映射表。 经过前端来断定登陆方式进行策略上的预处理,最终仍是会交给 UsernamePasswordAuthenticationFilter
。经过 HttpSecurity
的 addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
方法进行前置。
package cn.felord.spring.security.filter;
import cn.felord.spring.security.enumation.LoginTypeEnum;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY;
/** * 预登陆控制器 * * @author Felordcn * @since 16 :21 2019/10/17 */
public class PreLoginFilter extends GenericFilterBean {
private static final String LOGIN_TYPE_KEY = "login_type";
private RequestMatcher requiresAuthenticationRequestMatcher;
private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();
public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {
Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");
requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");
LoginPostProcessor loginPostProcessor = defaultLoginPostProcessor();
processors.put(loginPostProcessor.getLoginTypeEnum(), loginPostProcessor);
if (!CollectionUtils.isEmpty(loginPostProcessors)) {
loginPostProcessors.forEach(element -> processors.put(element.getLoginTypeEnum(), element));
}
}
private LoginTypeEnum getTypeFromReq(ServletRequest request) {
String parameter = request.getParameter(LOGIN_TYPE_KEY);
int i = Integer.parseInt(parameter);
LoginTypeEnum[] values = LoginTypeEnum.values();
return values[i];
}
/** * 默认仍是Form . * * @return the login post processor */
private LoginPostProcessor defaultLoginPostProcessor() {
return new LoginPostProcessor() {
@Override
public LoginTypeEnum getLoginTypeEnum() {
return LoginTypeEnum.FORM;
}
@Override
public String obtainUsername(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
}
@Override
public String obtainPassword(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
}
};
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper((HttpServletRequest) request);
if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
LoginTypeEnum typeFromReq = getTypeFromReq(request);
LoginPostProcessor loginPostProcessor = processors.get(typeFromReq);
String username = loginPostProcessor.obtainUsername(request);
String password = loginPostProcessor.obtainPassword(request);
parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);
parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);
}
chain.doFilter(parameterRequestWrapper, response);
}
}
复制代码
经过 POST
表单提交方式 http://localhost:8080/process?username=Felordcn&password=12345&login_type=0
能够请求成功。或者如下列方式也能够提交成功:
更多的方式 只须要实现接口 LoginPostProcessor
注入 PreLoginFilter
今天咱们经过各类技术的运用实现了从简单登陆到可动态扩展的多种方式并存的实战运用。相信对你来讲会有不小的收货 ,本次 **代码DEMO可经过关注公众号:Felordcn
回复 ss03
获取,后面会更加精彩。
关注公众号:Felordcn获取更多资讯