实现/oauth/token路由下可以适配所有的登录类型,自定义参数
0.准备
基于Spring Boot创建项目server-auth
https://start.spring.io/
在pom.xml添加lombok,并且idea安装了lombok插件(不会安装,百度一下)
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
使用idea打开项目,设置配置文件application.properties
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/cloud-auth?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
1.定义集成认证实体
@Data
public class IntegrationAuthenticationEntity {
private String authType;//请求登录认证类型
private Map<String,String[]> authParameters;//请求登录认证参数集合
public String getAuthParameter(String paramter){
String[] values = this.authParameters.get(paramter);
if(values != null && values.length > 0){
return values[0];
}
return null;
}
}
2.定义集成认证-认证器接口
public interface IntegrationAuthenticator {
/**
* 处理集成认证
* @param entity 集成认证实体
* @return 用户表实体
*/
UserPojo authenticate(IntegrationAuthenticationEntity entity);
/**
* 预处理
* @param entity 集成认证实体
*/
void prepare(IntegrationAuthenticationEntity entity);
/**
* 判断是否支持集成认证类型
* @param entity 集成认证实体
*/
boolean support(IntegrationAuthenticationEntity entity);
/**
* 认证结束后执行
* @param entity 集成认证实体
*/
void complete(IntegrationAuthenticationEntity entity);
}
3.定义集成认证-认证器抽象类
public abstract class AbstractPreparableIntegrationAuthenticator implements IntegrationAuthenticator {
@Override
public void prepare(IntegrationAuthenticationEntity entity) {
}
@Override
public void complete(IntegrationAuthenticationEntity entity) {
}
}
4.定义集成认证上下文
public class IntegrationAuthenticationContext {
private static ThreadLocal<IntegrationAuthenticationEntity> holder = new ThreadLocal<>();
public static void set(IntegrationAuthenticationEntity entity){
holder.set(entity);
}
public static IntegrationAuthenticationEntity get(){
return holder.get();
}
public static void clear(){
holder.remove();
}
}
5.定义集成认证拦截器
@Component
public class IntegrationAuthenticationFilter extends GenericFilterBean implements ApplicationContextAware {
private static final String AUTH_TYPE_PARM_NAME = "auth_type";//登录类型参数名
private static final String OAUTH_TOKEN_URL = "/oauth/token";//需要拦截的路由
private RequestMatcher requestMatcher;
private ApplicationContext applicationContext;
private Collection<IntegrationAuthenticator> authenticators;
public IntegrationAuthenticationFilter() {
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "GET"),
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "POST")
);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (requestMatcher.matches(request)){
RequestParameterWrapper requestParameterWrapper = new RequestParameterWrapper(request);
if (requestParameterWrapper.getParameter("password") == null){
requestParameterWrapper.addParameter("password","");
}
IntegrationAuthenticationEntity entity = new IntegrationAuthenticationEntity();
entity.setAuthType(requestParameterWrapper.getParameter(AUTH_TYPE_PARM_NAME));
entity.setAuthParameters(requestParameterWrapper.getParameterMap());
IntegrationAuthenticationContext.set(entity);
try {
this.prepare(entity);
filterChain.doFilter(requestParameterWrapper,servletResponse);
this.complete(entity);
} finally {
IntegrationAuthenticationContext.clear();
}
}
else {
filterChain.doFilter(servletRequest,servletResponse);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 认证前回调
* @param entity 集成认证实体
*/
private void prepare(IntegrationAuthenticationEntity entity) {
if (entity != null){
synchronized (this){
Map<String, IntegrationAuthenticator> map = applicationContext.getBeansOfType(IntegrationAuthenticator.class);
if (map != null){
this.authenticators = map.values();
}
}
}
if (this.authenticators == null){
this.authenticators = new ArrayList<>();
}
for (IntegrationAuthenticator authenticator : this.authenticators){
if (authenticator.support(entity)){
authenticator.prepare(entity);
}
}
}
/**
* 认证结束后回调
* @param entity 集成认证实体
*/
private void complete(IntegrationAuthenticationEntity entity) {
for (IntegrationAuthenticator authenticator: authenticators) {
if(authenticator.support(entity)){
authenticator.complete(entity);
}
}
}
/**
* 用途:在拦截时给Request添加参数
* Cloud OAuth2 密码模式需要判断Request是否存在password参数,
* 如果不存在会抛异常结束认证
* 所以在调用doFilter方法前添加password参数
*/
class RequestParameterWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> params = new HashMap<String, String[]>();
public RequestParameterWrapper(HttpServletRequest request) {
super(request);
this.params.putAll(request.getParameterMap());
}
public RequestParameterWrapper(HttpServletRequest request, Map<String, Object> extraParams) {
this(request);
addParameters(extraParams);
}
public void addParameters(Map<String, Object> extraParams) {
for (Map.Entry<String, Object> entry : extraParams.entrySet()) {
addParameter(entry.getKey(), entry.getValue());
}
}
@Override
public String getParameter(String name) {
String[]values = params.get(name);
if(values == null || values.length == 0) {
return null;
}
return values[0];
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
public void addParameter(String name, Object value) {
if (value != null) {
if (value instanceof String[]) {
params.put(name, (String[]) value);
} else if (value instanceof String) {
params.put(name, new String[]{(String) value});
} else {
params.put(name, new String[]{String.valueOf(value)});
}
}
}
}
}
6.定义用户表实体
@Data
public class UserPojo implements Serializable {
private Integer id;
private String name;
private String mobile;
private String mail;
private String pwd;
public UserPojo() {
}
}
7.集成认证-用户细节服务
@Service
public class IntegrationUserDetailsService implements UserDetailsService {
private List<IntegrationAuthenticator> authenticators;
@Autowired(required = false)
public void setIntegrationAuthenticators(List<IntegrationAuthenticator> authenticators) {
this.authenticators = authenticators;
}
@Override
public UserDetails loadUserByUsername(String str) throws UsernameNotFoundException {
IntegrationAuthenticationEntity entity = IntegrationAuthenticationContext.get();
if (entity == null){
entity = new IntegrationAuthenticationEntity();
}
UserPojo pojo = this.authenticate(entity);
if (pojo == null){
throw new UsernameNotFoundException("登录失败");
}
User user = new User(pojo.getName(),pojo.getPwd(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROOT_USER"));
return user;
}
private UserPojo authenticate(IntegrationAuthenticationEntity entity) {
if (this.authenticators != null) {
for (IntegrationAuthenticator authenticator : authenticators) {
if (authenticator.support(entity)) {
return authenticator.authenticate(entity);
}
}
}
return null;
}
}
8.启用Security
项目需要用到密码模式所以将AuthenticationManager添加到容器中,不需要用到密码模式,这步骤可以跳过
@EnableWebSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
9.启用授权服务器
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private IntegrationUserDetailsService integrationUserDetailsService;
//这里true,使全局密码结果为true,因为有些登录类型不需要验证密码,比如验证码登录,第三方系统登录等等,所以需要认证密码的要单独认证
@Bean
public PasswordEncoder passwordEncoder(){
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return "";
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return true;
}
};
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(integrationUserDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.authorizedGrantTypes("password")
.secret("server")
.scopes("all");
}
}
10.创建数据库colue-auth,执行SQL语句
数据库名:colue-auth,不是colue_auth
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`name` varchar(100) NOT NULL COMMENT '昵称',
`mobile` varchar(100) NOT NULL COMMENT '手机号',
`mail` varchar(100) NOT NULL COMMENT '邮箱',
`pwd` varchar(100) NOT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=83 DEFAULT CHARSET=utf8 COMMENT='用户表';
INSERT INTO user VALUES(NULL,'root','13555555555','10086@qq.com','$2a$10$hcMi5tIUGGGim/Xe0Z7q4e5Zz3QlK.EAek3an3nZf0B.ZdN0GJgSe')
11.定义用到UserMapper
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user WHERE name = #{name}")
public UserPojo findByName(String name);
@Select("SELECT * FROM user WHERE mobile = #{mobile}")
public UserPojo findByMobile(String mobile);
@Select("SELECT * FROM user WHERE mail = #{mail}")
public UserPojo findByMail(String mail);
}
12.定义密码登录认证器
@Component
@Primary
public class UsernamePasswordAuthenticator extends AbstractPreparableIntegrationAuthenticator {
@Autowired
private UserMapper mapper;
@Override
public UserPojo authenticate(IntegrationAuthenticationEntity entity) {
String name = entity.getAuthParameter("name");
String pwd = entity.getAuthParameter("pwd");
if(name == null || pwd == null){
throw new OAuth2Exception("用户名或密码不能为空");
}
UserPojo pojo = mapper.findByName(name);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
if(encoder != null && encoder.matches(pwd,pojo.getPwd())){
return pojo;
}
return null;
}
@Override
public boolean support(IntegrationAuthenticationEntity entity) {
return StringUtils.isEmpty(entity.getAuthType());
}
}
Postman执行效果
13.短信登录认证器
@Component
public class SmsAuthenticator extends AbstractPreparableIntegrationAuthenticator {
private final static String AUTH_TYPE = "sms";
@Autowired
private UserMapper mapper;
@Override
public UserPojo authenticate(IntegrationAuthenticationEntity entity) {
String mobile = entity.getAuthParameter("mobile");
if(StringUtils.isEmpty(mobile)){
throw new OAuth2Exception("手机号不能为空");
}
String code = entity.getAuthParameter("code");
//测试项目,所以将验证码顶死为:1234
if(! "1234".equals(code)){
throw new OAuth2Exception("验证码错误或已过期");
}
return mapper.findByMobile(mobile);
}
@Override
public boolean support(IntegrationAuthenticationEntity entity) {
return AUTH_TYPE.equals(entity.getAuthType());
}
}
Postman执行效果
总结
1.流程思路:通过拦截器IntegrationAuthenticationFilter拦截所有oauth/token请求,根据类型参数(参数名:auth_type)匹配对应认证器(在所有继承AbstractPreparableIntegrationAuthenticator类中调用support方法筛选),在匹配的成功的认证器调用authenticate方法执行用户认证处理。
2.扩展其他登录方式只要实现自定义的IntegrationAuthenticator就好了。