上一篇:https://blog.csdn.net/fengxianaa/article/details/124717243
账号密码是最常见的登录方式,但是现在的登录多种多样:手机验证码、二维码、第三方授权等等
下面模仿账号密码登录,新增一下手机验证码登录
修改 SecController 增加:
@GetMapping("/phone/code")
public String phoneCode(HttpSession session) throws IOException {
//验证码配置
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha kaptcha = new DefaultKaptcha();
kaptcha.setConfig(config);
//生成验证码
String code = kaptcha.createText();
session.setAttribute("phoneNum", code);
return code;
}
用户名密码登录用的是 UsernamePasswordAuthenticationToken,继承 AbstractAuthenticationToken
我们新建 PhoneNumAuthenticationToken 继承 AbstractAuthenticationToken
/**
* 模仿 UsernamePasswordAuthenticationToken
* 用来封装前端传过来的手机号、验证码
*/
public class PhoneNumAuthenticationToken extends AbstractAuthenticationToken {
private final Object phone;//手机号
private Object num;//验证码
public PhoneNumAuthenticationToken(Object phone, Object num) {
super(null);
this.phone = phone;
this.num = num;
setAuthenticated(false);
}
public PhoneNumAuthenticationToken(Object phone, Object num, Collection extends GrantedAuthority> authorities) {
super(authorities);
this.phone = phone;
this.num = num;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials() {
return num;
}
@Override
public Object getPrincipal() {
return phone;
}
}
之前的 UsernamePasswordAuthenticationFilter 拦截的是 /user/login 请求,从json中获取用户名、密码
参考 UsernamePasswordAuthenticationFilter 写一个过滤器,拦截短信登录接口/phone/login
新建 PhoneNumAuthenticationFilter 继承 AbstractAuthenticationProcessingFilter
/**
* 模仿 UsernamePasswordAuthenticationFilter 获取前端传递的 手机号、验证码
*/
public class PhoneNumAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 表示这个 Filter 拦截 /phone/login 接口
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER =
new AntPathRequestMatcher("/phone/login", "POST");
// 参数名
private String phoneParameter = "phone";
private String numParameter = "num";
public PhoneNumAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
/**
* 用来获取前端传递的手机号和验证码,然后调用 authenticate 方法进行认证
* @param request
* @param response
* @return
* @throws AuthenticationException
* @throws IOException
* @throws ServletException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("请求方式有误: " + request.getMethod());
}
//如果请求的参数格式不是json,直接异常
if (!request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
throw new AuthenticationServiceException("参数不是json:" + request.getMethod());
}
// 用户以json的形式传参的情况下
String phone = null;
String num = null;
try {
Map map = JSONObject.parseObject(request.getInputStream(),Map.class);
phone = map.get(phoneParameter);
num = map.get(numParameter);
} catch (IOException e) {
throw new AuthenticationServiceException("参数不对:" + request.getMethod());
}
if (phone == null) {
phone = "";
}
if (num == null) {
num = "";
}
phone = phone.trim();
// 封装手机号、验证码,后面框架会从中拿到 手机号, 调用我们的 LoginPhoneService 获取用户
PhoneNumAuthenticationToken authRequest
= new PhoneNumAuthenticationToken(phone, num);
//设置ip、sessionId信息
setDetails(request,authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected void setDetails(HttpServletRequest request, PhoneNumAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
新建 LoginPhoneService
@Component
public class LoginPhoneService implements UserDetailsService {
@Autowired
private UserService userService;
/**
* 根据手机号查询用户对象
* @param phone 前端传的手机号
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
// 从数据库查询用户
User user = userService.getByPhone(phone);
if(user == null){
return null;
}
// 把用户信息封装到一个 userdetails 对象中,UserDetails是一个接口,LoginUser实现了这个接口
LoginUser loginUser = new LoginUser();
loginUser.setUser(user);
return loginUser;
}
}
注意,这里需要修改数据库的user表,增加 phone 字段
之前说过:this.getAuthenticationManager().authenticate(authRequest); 这句代码,其中的 authenticate 方法封装了具体的用户名、密码热证逻辑,其实里面是调用了 DaoAuthenticationProvider 的 authenticate 方法
用户登录的方式有很多种,每一种都有特定的 Provider 负责处理,
DaoAuthenticationProvider 就是负责验证用户名、密码这种方式的登录
我们的手机号、验证码登录,需要自己创建一个 Provider
新建 PhoneAuthenticationProvider 实现 AuthenticationProvider 接口,主要实现 authenticate 方法,写我们自己的认证逻辑
/**
* 主要实现 authenticate 方法,写我们自己的认证逻辑
*/
@Component
public class PhoneAuthenticationProvider implements AuthenticationProvider {
@Autowired
private LoginPhoneService loginPhoneService;
/**
* 手机号、验证码的认证逻辑
* @param authentication 其实就是我们封装的 PhoneNumAuthenticationToken
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PhoneNumAuthenticationToken token = (PhoneNumAuthenticationToken) authentication;
String phone = (String) authentication.getPrincipal();// 获取手机号
String num = (String) authentication.getCredentials(); // 获取输入的验证码
// 1. 从 session 中获取验证码
HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String phoneNum = (String) req.getSession().getAttribute("phoneNum");
if (!StringUtils.hasText(phoneNum)) {
throw new BadCredentialsException("验证码已经过期,请重新发送验证码");
}
if (!phoneNum.equals(num)) {
throw new BadCredentialsException("验证码不正确");
}
// 2. 根据手机号查询用户信息
LoginUser loginUser = (LoginUser) loginPhoneService.loadUserByUsername(phone);
if (loginUser == null) {
throw new BadCredentialsException("用户不存在,请注册");
}
// 3. 把用户封装到 PhoneNumAuthenticationToken 中,
// 后面就可以使用 SecurityContextHolder.getContext().getAuthentication(); 获取当前登陆用户信息
PhoneNumAuthenticationToken authenticationResult = new PhoneNumAuthenticationToken(loginUser, num, loginUser.getAuthorities());
authenticationResult.setDetails(token.getDetails());
return authenticationResult;
}
/**
* 判断是上面 authenticate 方法的 authentication 参数,是哪种类型
* Authentication 是个接口,实现类有很多,目前我们最熟悉的就是 PhoneNumAuthenticationToken、UsernamePasswordAuthenticationToken
* 很明显,我们只支持 PhoneNumAuthenticationToken,因为它封装的是手机号、验证码
* @param authentication
* @return
*/
@Override
public boolean supports(Class> authentication) {
// 如果参数是 PhoneNumAuthenticationToken 类型,返回true
return (PhoneNumAuthenticationToken.class.isAssignableFrom(authentication));
}
}
下面最重要的,把上面的东西配置到 SecurityConfig 中,让其生效
@Bean
public PhoneNumAuthenticationFilter phoneNumAuthenticationFilter() throws Exception {
PhoneNumAuthenticationFilter filter = new PhoneNumAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());//认证使用
//设置登陆成功返回值是json
filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(JSONObject.toJSONString(authentication));
}
});
//设置登陆失败返回值是json
filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
Map map = new HashMap<>();
map.put("errMsg", "手机登陆失败:"+ exception.getMessage());
out.write(JSONObject.toJSONString(map));
out.flush();
out.close();
}
});
filter.setFilterProcessesUrl("/phone/login");//其实这里不用设置,在 PhoneNumAuthenticationFilter 我们已经定义了一个静态变量
return filter;
}
@Autowired
private LoginUserService loginUserService;
/**
* DaoAuthenticationProvider 是默认做账户密码认证的,现在有两种登录方式,手机号和账户密码
* 如果不在这里声明,账户密码登录不能用
* @return
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
//对默认的UserDetailsService进行覆盖
authenticationProvider.setUserDetailsService(loginUserService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
@Autowired
private PhoneAuthenticationProvider phoneAuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// /phone/code 请求不用登陆
.antMatchers("/code","/phone/code").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
http.addFilterAt(myUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(daoAuthenticationProvider());//把账户密码验证加进去
//把 手机号认证过滤器 加到拦截器链中
http.addFilterAfter(phoneNumAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(phoneAuthenticationProvider);//把验证逻辑加进去
}
不登录访问 localhost:8080/sec
获取手机验证码
输入错误的验证码
输入正确的,登录成功
其实上面的代码可以优化,根据上面的代码逻辑,我们是先根据手机号拿到用户后,再比较验证码是否正确
根据我们之前账户密码登录的经验,比较验证码是否正确的代码完全可以放到 PhoneNumAuthenticationFilter 中
但是为了模仿账号密码登录的这个过程,我并没有那样做