Created
June 10, 2021 06:28
-
-
Save alsritter/6bf27321a0364934d4fce42f1d63de2d to your computer and use it in GitHub Desktop.
这里自定义一个多种验证方式的 AuthenticationProcessingFilter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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