上一篇文章,通过简单的方式进行自定义用户登录授权的动作,投机取巧使用了他的内部循环处理机制,完成了我们所需要的功能,本篇将介绍一下自定义模式的登录方式(例如:原始oauth2自带的密码模式,授权码模式,简单模式,客户端模式)。最终其实我们还是采用了密码模式的思路,只是修改了两个参数而已。
其中,Token的关键在于Authenticated是否授权,需要两个构造,一个为未鉴权Token,以及一个已鉴权Token,后续也需要判断是否为该Token,才进行相关判断
package com.example.customoauth.authentication.account;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
我个人将验证码以及密码模式封装在一起,所以传入三个参数,通过判断判断短信验证码是否为空来决定采用哪种验证方式,是密码验证还是短信验证码验证,
**/
public class AccountLoginToken extends AbstractAuthenticationToken {
//用户名
private final Object principal;
//密码
private Object credentials;
//短信验证码
private String vcode;
/**
* 构建一个没有鉴权的 AccountLoginToken,手机号,密码,验证码
*/
public AccountLoginToken(Object principal, Object credentials,String vcode) {
super(null);
this.principal = principal;
this.credentials=credentials;
this.vcode = vcode;
setAuthenticated(false);
}
/**
* 构建拥有鉴权的 AccountLoginToken
*/
public AccountLoginToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
public String getVcode() {
return this.vcode;
}
}
此功能在第一篇已经介绍,不过多赘述,直接贴入代码
package com.example.customoauth.authentication.account;
import com.alibaba.fastjson.JSON;
import com.yfhcloud.common.yfhutils.Constant;
import com.yfhcloud.common.yfhutils.WebResponse;
import com.yfhcloud.common.yfhutils.enums.ResultEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
/**
* 后端短信认证校验
* 原理:AuthenticationProvider通常按照认证请求链顺序去执行,
* 一个返回非null响应表示程序验证通过,
* 不再尝试验证其它的provider;如果后续提供的身份验证程序 成功地对请求进行身份认证,
* 则忽略先前的身份验证异常及null响应,并将使用成功的身份验证。
* 如果没有provider提供一个非null响应,或者有一个新的抛出AuthenticationException,
* 那么最后的AuthenticationException将会抛出。
*
* @author : Windy
* @version :1.0
* @since : 2020/12/22 15:51
*/
@Component
@Slf4j
public class AccountAuthenticationProvider implements AuthenticationProvider {
//这个代码我就不贴了,上一篇有,根据自身业务进行修改即可
@Autowired
@Qualifier("securityUserDetailsService")
private UserDetailsService securityUserDetailsService;
//由于使用@Autowired进行加载,似乎有冲突,暂时我默认直接在用的地方加载,第一篇那地方也需要修改以下,暂时不在config中用@bean进行初始化。
private PasswordEncoder passwordEncoder= PasswordEncoderFactories.createDelegatingPasswordEncoder();
//注入REDIS,存放短信验证码使用
@Autowired
private RedisTemplate redisTemplate;
//认证方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AccountLoginToken accountLoginToken = (AccountLoginToken) authentication;
System.out.println("===进入USER登录验证环节=====" + JSON.toJSONString(accountLoginToken));
UserDetails userDetails = securityUserDetailsService.loadUserByUsername(accountLoginToken.getName());
if (StringUtils.isNotBlank(accountLoginToken.getVcode())) {
//如果vcode不为空,则为短信验证码校验
if (redisTemplate.opsForValue().get(Constant.REDIS_SMS_LOGIN_CODE + accountLoginToken.getName()) != null) {
String redisCode = redisTemplate.opsForValue().get(Constant.REDIS_SMS_LOGIN_CODE + accountLoginToken.getName()).toString();
if (!redisCode.equalsIgnoreCase(accountLoginToken.getVcode())) {
throw new BadCredentialsException("验证码不正确");
}
} else {
throw new BadCredentialsException("验证码已失效!");
}
//校验成功,返回一个授权过的Token
return new AccountLoginToken(userDetails, userDetails.getAuthorities());
} else {
//其他模式直接采用用户密码判断
if (passwordEncoder.matches(authentication.getCredentials().toString(), userDetails.getPassword())) {
//返回一个授权过的Token
return new AccountLoginToken(userDetails, userDetails.getAuthorities());
}
}
throw new BadCredentialsException("用户名密码不正确");
}
//判断是否支持自定义的Token,从而决定是否需要进行认证校验,当使用accountLoginToken进行校验
@Override
public boolean supports(Class<?> authentication) {
return AccountLoginToken.class.isAssignableFrom(authentication);
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(securityUserDetailsService);
auth.authenticationProvider(adminSmsAuthenticationProvider);
auth.authenticationProvider(adminPwdAuthenticationProvider);
auth.authenticationProvider(accountAuthenticationProvider);
}
该类主要进行的就是封装的token传入到security的authenticationManager进行校验动作,在上一篇也描述了,密码模式采用的UserPassword**Token进行处理,这里我们需要修改成自定义的AccountLoginToken传入到authenticationManager中。
AbstractTokenGranter中的granter方法,主要先校验clientId,后校验用户信息。所以我们主要重写的getOAuth2Authentication方法,进行后续处理
package com.example.customoauth.authentication.account;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 自定义模式
* @author : Windy
* @version :1.0
* @since : 2020/12/28 15:47
*/
public class AccountGranter extends AbstractTokenGranter {
//模式名称
public static final String GRANT_TYPE = "account_mobile";
private final AuthenticationManager authenticationManager;
public AccountGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager) {
super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
//此处map为controller封装的参数map,名字也可以自己定义
//从参数中获取手机号
String mobile = parameters.get("username");
//参数中获取密码
String password = parameters.get("password");
//参数中获取密码
String vcode = parameters.get("vcode");
//删除参数中的两项(这里我暂时还不清楚为啥要删除,不清楚这个传输会引起什么问题,源码中删除那么我先暂时删除)
parameters.remove("password");
parameters.remove("vcode");
//暂时将三个全部传入
Authentication userAuth = new AccountLoginToken(mobile, password,vcode);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
}catch (AccountStatusException | BadCredentialsException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
}
// If the username/password are wrong the spec says we should send 400/invalid grant
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("校验失败: " + mobile);
}
//验证通过,则返回一个Oauth2token。
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
package com.example.customoauth.config;
import com.example.customoauth.authentication.account.AccountGranter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author : Windy
* @version :1.0
* @since : 2020/12/8 16:19
*/
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 用户认证 Manager
*/
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
//放入自定义授权模式,先把默认的几个模式放进去。最后加入我们自定义模式。
List<TokenGranter> grantersList = new ArrayList<TokenGranter>(Arrays.asList(endpoints.getTokenGranter()));
grantersList.add(new AccountGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(), authenticationManager));
endpoints.tokenGranter(new CompositeTokenGranter(grantersList));
//此处list并不是5,而是2,因为第一个是一个config,具体你们可以在启动的时候调试看内部信息。
System.out.println("=====长度:"+grantersList.size());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients();
oauthServer.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//此处要注意给这个client进行授权新模式,否则校验不通过
clients.inMemory() // <4.1>
.withClient("clientapp").secret("{noop}112233") // <4.2> Client 账号、密码。
.authorizedGrantTypes("password",AccountGranter.GRANT_TYPE) // <4.2> 密码模式
.scopes("read_userinfo", "read_contacts") // <4.2> 可授权的 Scope
// .and().withClient() // <4.3> 可以继续配置新的 Client
;
}
}
package com.example.customoauth.web;
import com.example.customoauth.authentication.account.AccountGranter;
import com.example.customoauth.authentication.account.AccountLoginToken;
import com.example.customoauth.request.UserRequest;
import com.yfhcloud.common.yfhutils.WebResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* @author : Windy
* @version :1.0
* @since : 2020/12/9 16:44
*/
@RestController
@RequestMapping("/")
@Slf4j
public class LoginController {
@Autowired
private OAuth2ClientProperties oauth2ClientProperties;
@Autowired
private TokenEndpoint tokenEndpoint;
@PostMapping("/login/admin")
public WebResponse adminLogin(UserRequest request) {
Map<String, String> parameters = createClientParameters(request.getPhone(),request.getVcode(),"password");
try {
OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(createClientToken(), parameters).getBody();
return WebResponse.success(oAuth2AccessToken);
} catch (Exception e) {
log.error("登录失败!!" + e.getMessage());
}
return WebResponse.error("登录失败,请重试!");
}
@PostMapping("/login/user")
public WebResponse userLogin(UserRequest request) {
//封装所需要的参数
Map<String, String> parameters = createClientParameters(request.getPhone(),request.getPassword(), AccountGranter.GRANT_TYPE);
//放入可能需要的vcode,后续TokenGranter需要
parameters.put("vcode",request.getVcode());
User u = new User(oauth2ClientProperties.getClientId(), oauth2ClientProperties.getClientSecret(), new ArrayList<>());
//生成验证过的clientUsertoken
AccountLoginToken token = new AccountLoginToken(u,new ArrayList<>());
try {
OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(token, parameters).getBody();
return WebResponse.success(oAuth2AccessToken);
} catch (Exception e) {
log.error("登录失败!!" + e.getMessage());
}
return WebResponse.error("登录失败,请重试!");
}
//构造客户端请求
private Map<String, String> createClientParameters(String username, String password,String grantType) {
Map<String, String> parameters = new HashMap<String, String>();
parameters.put("username", username);
parameters.put("password", password);
parameters.put("grant_type", grantType);
return parameters;
}
//构造客户端Token
private UsernamePasswordAuthenticationToken createClientToken() {
//客户端信息
User u = new User(oauth2ClientProperties.getClientId(), oauth2ClientProperties.getClientSecret(), new ArrayList<>());
//生成已经认证的client
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(u, null, new ArrayList<>());
return token;
}
}
至此,整个登录就结束了,这种好处是没有那么多provider去循环。毕竟在模式中就已经选定了,目前这两种应该是按照security oauth2的逻辑进行重写,实现这套登录逻辑,网上很多在filter中去写校验我个人认为还是有点不合理。违背了源码中的设计方式。
目前仅仅将登录这部分代码整理清楚,源码我暂时还不对外发布到git上,毕竟现在还是个半成品,连client都还在内存中,这肯定不合理。等后续全部写好,并上线,我再将整个模块放上去,在此,有任何疑问或者问题,都可以在评论区留言,我这边有问题会及时回复,共同探讨,,如果对我的理解有不同的理解和看法,欢迎指正,共同分析源码,找出依据,完善整个思路。
可惜spring security oauth2在3.4以上版本似乎全部被淘汰。只能等全新的 【spring-authorization-server】项目成熟上线。后续我将持续关注。有新的权限框架,我会继续更新。
大家在使用【spring-authorization-server】这个项目里的demo进行试验的时候一定一定要注意,不要修改里面的任何地址,包括里面有个设置,需要你在hosts中设置虚拟域名指向,也一定要去设置,如果你贸然改成localhost将会出现问题,导致不能使用,具体原因我也不太清楚。所以一定要设置hosts虚拟域名。不会设置请百度。
接下来,我会写一些spring security oauth2鉴权方面的文章,如果不复杂,我将找几篇好的文章分享给大家,我就不过多赘述,坚决不抄袭,只负责整理,不会把别人的文章全部抄过来或者转载,最多发链接和自己的见解,拒绝搬运,只做原创。