Skip to content

Instantly share code, notes, and snippets.

@alsritter
Created June 10, 2021 06:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alsritter/6bf27321a0364934d4fce42f1d63de2d to your computer and use it in GitHub Desktop.
Save alsritter/6bf27321a0364934d4fce42f1d63de2d to your computer and use it in GitHub Desktop.
这里自定义一个多种验证方式的 AuthenticationProcessingFilter
package com.alsritter.oauth2.config;
import com.alsritter.oauth2.provider.Provider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* filter 用于接收网络请求,调用 manager 进行认证
* manager 管理多个 provider,选择合适的 provider 进行认证
* provider 负责认证,检查账号密码之类的,返回 token
* token 存储认证信息,包含账号密码,具体可以自己定义
*
* @author alsritter
* @version 1.0
**/
public class AuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
/**
* 设置默认的认证 url 地址
*/
protected AuthenticationProcessingFilter() {
super("/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse httpServletResponse)
throws AuthenticationException, IOException, ServletException {
// 从前端取得 type 参数
String type = request.getParameter("type");
// 策略模式
Provider provider = new Provider(type);
return getAuthenticationManager().authenticate(provider.executeStrategy(request));
}
}
package com.alsritter.oauth2.provider;
import com.alsritter.oauth2.domain.SecurityUser;
import com.alsritter.oauth2.service.UserService;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 编写一个自定的 AuthenticationProvider
*
* @author alsritter
* @version 1.0
**/
@Component
@Setter(onMethod_ = {@Autowired})
public class PhoneAuthenticationProvider implements AuthenticationProvider, ProviderStrategy {
private UserService userService;
/**
* 从请求中取得登陆的参数
*
* @return 返回一个待认证的 Token
*/
@Override
public Authentication authenticate(HttpServletRequest request) {
String phone = request.getParameter("phone");
String code = request.getParameter("code");
String realCode = (String) request.getSession().getAttribute("phoneCode");
return new PhoneAuthenticationToken(phone, code, realCode);
}
/**
* 认证代码,认证通过返回认证对象,失败返回 null
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PhoneAuthenticationToken token = (PhoneAuthenticationToken) authentication;
if (token.getPhone() == null || token.getCode() == null || token.getRealCode() == null) {
return null;
}
//检查验证码
if (!token.getCode().equals(token.getRealCode())) {
return null;
}
//根据手机号获取用户信息
SecurityUser user = userService.getUserPhone(token.getPhone());
if (user != null) {
// 写入用户信息并返回认证类
return new PhoneAuthenticationToken(user, user.getAuthorities());
}
return null;
}
/**
* Manager 传递 token 给 provider 时会调用本方法判断该 provider 是否支持该 token。
* 不支持则尝试下一个 filter
* 本类支持的 token 类: PhoneAuthenticationProvider
*
* @return 当前 provider 是否支持该 token。
*/
@Override
public boolean supports(Class<?> authentication) {
return (PhoneAuthenticationProvider.class.isAssignableFrom(authentication));
}
}
package com.alsritter.oauth2.provider;
import com.alsritter.oauth2.domain.SecurityUser;
import lombok.Getter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 自定义一个 AuthenticationToken 对象,对标 UsernamePasswordAuthenticationToken 这个实现类
* 被认证后的 AuthenticationToken 会被填充到 SecurityContextHolder 安全上下文容器中
*
* @author alsritter
* @version 1.0
**/
@Getter
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
//用户信息
private SecurityUser user;
private String phone;
// 用户输入的验证码
private String code;
// 真正的验证码
private String realCode;
/**
* 注意这个构造方法是认证时使用的
*/
public PhoneAuthenticationToken(String phone, String code, String realCode) {
super(null);
this.phone = phone;
this.code = code;
this.realCode = realCode;
super.setAuthenticated(false); //标记未认证
}
/**
* 注意这个构造方法是认证成功后使用的
*/
public PhoneAuthenticationToken(SecurityUser user, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.user = user;
super.setAuthenticated(true); //标记已认证
}
@Override
public Object getCredentials() {
return null;
}
/**
* 获取用户信息
*/
@Override
public Object getPrincipal() {
return this.user;
}
/**
* 这里的重写单纯就是为了抑制警告
*/
@Override
public boolean equals(Object o) {
return super.equals(o);
}
@Override
public int hashCode() {
return super.hashCode();
}
}
package com.alsritter.oauth2.provider;
import org.springframework.security.core.Authentication;
import javax.servlet.http.HttpServletRequest;
/**
* 策略模式,返回对应的 AuthenticationProvider
*
* @author alsritter
* @version 1.0
**/
public class Provider {
private final ProviderStrategy providerStrategy;
/**
* 根据 type 选择 provider
*
* @param type 登陆的类型
*/
public Provider(String type) {
//默认密码登录,如果传入一个不存在的类型,就使用密码登录
switch (type) {
case "phone":
this.providerStrategy = new PhoneAuthenticationProvider();
break;
case "email":
this.providerStrategy = new EmailAuthenticationProvider();
break;
default:
this.providerStrategy = new PasswordAuthenticationProvider();
break;
}
}
/**
* 执行某 provider 的认证方法
*/
public Authentication executeStrategy(HttpServletRequest request) {
return providerStrategy.authenticate(request);
}
}
package com.alsritter.oauth2.provider;
import org.springframework.security.core.Authentication;
import javax.servlet.http.HttpServletRequest;
/**
* 这里创建一个接口用来传入 HttpServletRequest
*
* @author alsritter
* @version 1.0
**/
public interface ProviderStrategy {
Authentication authenticate(HttpServletRequest request);
}
package com.alsritter.oauth2.config;
import com.alsritter.oauth2.provider.EmailAuthenticationProvider;
import com.alsritter.oauth2.provider.PasswordAuthenticationProvider;
import com.alsritter.oauth2.provider.PhoneAuthenticationProvider;
import com.alsritter.oauth2.service.UserService;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 配置 Spring Security
* <p>
* 这里参考自:SpringCloud OAuth2 JWT认证 多种登录方式
* https://juejin.cn/post/6914933029836324871
*
* @author alsritter
* @version 1.0
**/
@Configuration
@EnableWebSecurity
@AllArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
/**
* 注入三个 provider,对应密码,手机号,邮箱三种登录方式
*/
@Bean
PasswordAuthenticationProvider passwordAuthenticationProvider() {
return new PasswordAuthenticationProvider();
}
@Bean
PhoneAuthenticationProvider phoneAuthenticationProvider() {
return new PhoneAuthenticationProvider();
}
@Bean
EmailAuthenticationProvider emailAuthenticationProvider() {
return new EmailAuthenticationProvider();
}
/**
* 这里把 UsernamePasswordAuthenticationFilter 换成自定义的 AuthenticationProcessingFilter 过滤器
* <p>
* 先来回顾一下默认的认证过程:
* <p>
* 1、用户提交用户名、密码被 SecurityFilterChain 中的 UsernamePasswordAuthenticationFilter 过滤器获取到,
* 封装为请求Authentication,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
* <p>
* 2、然后过滤器将 Authentication 提交至认证管理器(AuthenticationManager)进行认证
* <p>
* 3、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的
* (包括权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。
* <p>
* 4、SecurityContextHolder 安全上下文容器通过 `SecurityContextHolder.getContext().setAuthentication(…)`
* 方法将第 3 步填充了信息的 Authentication 填充进来。
* <p>
* <p>
* 可以看出 AuthenticationManager 接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为 ProviderManager。
* 而 Spring Security 支持多种认证方式,因此 ProviderManager 维护着一个 `List<AuthenticationProvider>` 列表,存放多种认证方式,
* 最终实际的认证工作是由 AuthenticationProvider 完成的。
*/
AuthenticationProcessingFilter authenticationProcessingFilter(AuthenticationManager authenticationManager) {
AuthenticationProcessingFilter userPasswordAuthenticationProcessingFilter = new AuthenticationProcessingFilter();
//为filter设置管理器
userPasswordAuthenticationProcessingFilter.setAuthenticationManager(authenticationManager);
//登录成功后跳转到 OAuth2 的认证地址(这里地址填网关的)
userPasswordAuthenticationProcessingFilter.setAuthenticationSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {
// 这里无需担心 oauth/authorize 端点如何拿到用户信息的,它位于 AuthorizationEndpoint 类里,
// 通过 Principal 参数可以拿到 Token 的信息(user、authorities 之类的)
httpServletResponse.sendRedirect(
"http://localhost:9527/auth/oauth/authorize?" +
"client_id=c1" +
"&response_type=token" +
"&scope=all" +
"&redirect_uri=http://localhost:9527/auth/token.html");
});
//登录失败后跳转
userPasswordAuthenticationProcessingFilter.setAuthenticationFailureHandler((httpServletRequest, httpServletResponse, authentication) -> {
httpServletResponse.sendRedirect("/login/failure");
});
return userPasswordAuthenticationProcessingFilter;
}
// //配置管理器
// @Autowired
// public void configureGlobal(AuthenticationManagerBuilder auth) {
// //往管理器中添加provider
// auth.authenticationProvider(passwordAuthenticationProvider())
// .authenticationProvider(phoneAuthenticationProvider())
// .authenticationProvider(emailAuthenticationProvider());
// }
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
public void configure(WebSecurity web) {
// 忽略哪些资源不用 security 来管理
web.ignoring().antMatchers("/rsa/publicKey"); // 注意不要忽略 /oauth/token 不然它不会走过滤器
}
/**
* 认证用户的来源
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
// super.configure(auth);
auth.authenticationProvider(passwordAuthenticationProvider())
.authenticationProvider(phoneAuthenticationProvider())
.authenticationProvider(emailAuthenticationProvider());
}
/**
* 配置 SpringSecurity 相关信息
* 允许匿名访问所有接口 主要是 oauth 接口
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 先关闭 CSRF 防护
.cors()
.and()
// 使用表单登陆
.formLogin()
.loginPage("/login/noLogin"); //未登录跳转到此接口
// .authorizeRequests() // 允许使用 RequestMatcher 实现(即通过URL模式)基于 HttpServletRequest 限制访问
// .antMatchers(HttpMethod.OPTIONS).permitAll(); // 这是跨域请求时浏览器会发的预请求,这里直接放行
// 把自定义的过滤器加载 UsernamePasswordAuthenticationFilter 这个默认过滤器签名
http.addFilterBefore(authenticationProcessingFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
// http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //禁用session
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment