序言
上一篇文章我们跟踪源码了解到了security整个的登陆流程,这一篇我们就基于这个流程来做一个自己定义的流程,并与security对接。本篇中介绍的是短信登陆,当然验证码登陆跟这个原理是一样的就不多说了。
代码请参考 https://github.com/AutismSuperman/springsecurity-example
准备页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h3>验证码表单登录</h3>
<table>
<tr>
<td>用户名:</td>
<td><input type="text" autocomplete="off" name="mobile"></td>
</tr>
<tr>
<td>验证码:</td>
<td><input type="password" autocomplete="off" name="sms_code"></td>
</tr>
<tr>
<td colspan="2">
<a href="javascript:void(0);" onclick="sendSms()">获取验证码</a>
<button type="button" onclick="loginSms()">登录</button>
</td>
</tr>
</table>
<h3>普通表单登录</h3>
<table>
<tr>
<td>用户名:</td>
<td><input type="text" autocomplete="off" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" autocomplete="off" name="password"></td>
</tr>
<tr>
<td colspan="2">
<button type="button" onclick="login()">登录</button>
</td>
</tr>
</table>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
function sendSms() {
window.open('/sms/code?mobile=' + $("input[name=mobile]").val());
}
function loginSms() {
var mobile = $("input[name=mobile]").val();
var smsCode = $("input[name=sms_code]").val();
if (mobile === "" || smsCode === "") {
alert("用户名或密码不能为空");
return;
}
$.ajax({
type: "POST",
url: "/sms/login",
data: {
"mobile": mobile,
"smsCode": smsCode
},
success: function (e) {
console.log(e);
alert("登陆成功")
setTimeout(function () {
location.href = '/hello';
}, 500);
},
error: function (e,a,b) {
console.log(a);
console.log(b);
console.log(e.responseText);
alert("登陆失败zxczxczc")
}
});
}
function login() {
var username = $("input[name=username]").val();
var password = $("input[name=password]").val();
if (username === "" || password === "") {
alert("用户名或密码不能为空");
return;
}
debugger
$.ajax({
type: "POST",
url: "/authentication/form",
data: {
"username": username,
"password": password
},
success: function (e) {
console.log(e);
alert("登陆成功")
setTimeout(function () {
location.href = '/hello';
}, 500);
},
error: function (e,a,b) {
console.log(e.responseText);
alert("登陆失败zxczxczc")
}
});
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<h2>hello world from fulinlin.</h2>
<a href="/logout">退出登录</a>
</body>
</html>
@Controller
public class LoginController {
@RequestMapping("/login")
public String login() {
return "login";
}
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
发短信的接口
@RestController
@Slf4j
public class SmsController {
@RequestMapping("/sms/code")
public String sms(String mobile, HttpSession session) {
int code = (int) Math.ceil(Math.random() * 9000 + 1000);
Map<String, Object> map = new HashMap<>(16);
map.put("mobile", mobile);
map.put("code", code);
session.setAttribute("smsCode", map);
log.info("{}:为 {} 设置短信验证码:{}", session.getId(), mobile, code);
return "你的手机号"+mobile+"验证码是"+code;
}
}
准备测试用户
public interface IUserService {
SysUser findByUsername(String userName);
}
实现类
@Service
public class UserServiceImpl implements IUserService {
private static final Set<SysUser> users = new HashSet<>();
static {
users.add(new SysUser(1L, "fulin", "123456", Arrays.asList("ROLE_ADMIN", "ROLE_DOCKER")));
users.add(new SysUser(2L, "xiaohan", "123456", Arrays.asList("ROLE_ADMIN", "ROLE_DOCKER")));
users.add(new SysUser(3L, "longlong", "123456", Arrays.asList("ROLE_ADMIN", "ROLE_DOCKER")));
}
@Override
public SysUser findByUsername(String userName) {
return users.stream().filter(o -> StringUtils.equals(o.getUserName(), userName)).findFirst().orElse(null);
}
}
UserDetailsService
@Service
public class UserService implements UserDetailsService {
@Autowired
private IUserService iUserService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
SysUser user = iUserService.findByUsername(s);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
//把角色放入认证器里
Collection<GrantedAuthority> authorities = new ArrayList<>();
List<String> roles = user.getRoles();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return new User(user.getUserName(), user.getPassword(), authorities);
}
}
AuthenticationFilter
首先呢我们要有一个自定义的 AuthenticationFilter
来实现对自定义登陆的拦截,我们模仿源码去写,拿到请求中的 mobile
构建一个没有认证的 SmsCodeAuthenticationToken
/**
* 短信登录的鉴权过滤器,模仿 UsernamePasswordAuthenticationFilter 实现
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* form表单中手机号码的字段name
*/
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
/**
* 是否仅 POST 方式
*/
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
// 短信登录的请求 post 方式的 /sms/login
super(new AntPathRequestMatcher("/sms/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public String getMobileParameter() {
return mobileParameter;
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
AuthenticationToken
上面说到了构建一个没有认证的SmsCodeAuthenticationToken
那么我们就来自己写一个
/*
*这一步的作用是为了替换原有系统的 UsernamePasswordAuthenticationToken 用来做验证
*
* 代码都是从UsernamePasswordAuthenticationToken 里粘贴出来的
*
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
* 在这里就代表登录的手机号码
*/
private final Object principal;
/**
* 构建一个没有鉴权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
/**
* 构建拥有鉴权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// 剩下的方法不用动就行了 就是从 UsernamePasswordAuthenticationToken 里粘贴出来的
// ========================================================================================================
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
AuthenticationProvider
有了AuthenticationFilter
AuthenticationToken
都有了,按照上一篇的逻辑走,肯定要有一个AuthenticationProvider
来进行验证吧。那么我们也来写一个
/**
* 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
*/
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
//上下文中的 userDetailsService
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
String mobile = (String) authenticationToken.getPrincipal();
checkSmsCode(mobile);
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
// 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
private void checkSmsCode(String mobile) {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String inputCode = request.getParameter("smsCode");
//这里的验证码我们放session里,这里拿出来跟用户输入的做对比
Map<String, Object> smsCode = (Map<String, Object>) request.getSession().getAttribute("smsCode");
if (smsCode == null) {
throw new BadCredentialsException("未检测到申请验证码");
}
String applyMobile = (String) smsCode.get("mobile");
int code = (int) smsCode.get("code");
if (!applyMobile.equals(mobile)) {
throw new BadCredentialsException("申请的手机号码与登录手机号码不一致");
}
if (code != Integer.parseInt(inputCode)) {
throw new BadCredentialsException("验证码错误");
}
}
@Override
public boolean supports(Class<?> authentication) {
// 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
SecurityConfigurerAdapter
AuthenticationFilter
AuthenticationToken
AuthenticationProvider
全部都有了,那么还差啥?
就差跟 security做绑定了怎么做呢? 不慌我们先定义下成功处理器和失败处理器
成功处理器
@Component
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
log.info("登录成功");
response.setStatus(HttpStatus.OK.value());
ModelMap modelMap = GenerateModelMap.generateMap(HttpStatus.OK.value(), "登录成功");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(modelMap));
}
}
失败处理器
@Component
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("登录失败!");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
ModelMap modelMap = GenerateModelMap.generateMap(HttpStatus.INTERNAL_SERVER_ERROR.value(), "验证失败");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(modelMap));
}
}
加入到过滤链里
SecurityConfigurerAdapter
顾名思义就是 SecurityConfigurer的适配器,我们只需要吧我们刚才写的 AuthenticationFilter
AuthenticationToken
AuthenticationProvider
都放进来就可以与security挂上了。
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired //我们自己定义的UserDetailsService
private UserService userService;
@Autowired
private AuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler customAuthenticationFailureHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
//设置AuthenticationManager
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//设置失败成功处理器
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
//设置UserDetailsService
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userService);
//这里说明要把我们自己写的Provider放在过滤链的哪里
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
这样只是加入到了security的过滤链里 但是并没有生效,那么怎么配置呢?对就是还要在 WebSecurityConfigurerAdapter
里配置一下。
WebSecurityConfigurerAdapter
要想让 咱们自定义的配置生效,必须在配置中加入 http.apply(config)
才可以。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired //注入咱们自己定义的登陆流程
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Autowired
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(
new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单登陆配置
http.formLogin()
.failureHandler(customAuthenticationFailureHandler)
.successHandler(customAuthenticationSuccessHandler)
.loginPage("/login")
.loginProcessingUrl("/authentication/form")
.and();
http.apply(smsCodeAuthenticationSecurityConfig)
.and()
.logout()
.logoutUrl("/logout")
.and()
.authorizeRequests()
// 如果有允许匿名的url,填在下面
.antMatchers("/login", "/sms/**", "/authentication/form").permitAll()
.anyRequest().authenticated();
// 关闭CSRF跨域
http.csrf().disable();
}
}
至此我们已经完成了我们自定义的登陆流程。
本博文是基于springboot2.x 和security 5 如果有什么不对的请在下方留言。