一:简介
通过短信验证码获取令牌的过程也是自定义认证的过程。 组定义认证流程如下:
自定义一个过滤器实现认证 SmsCodeAuthenticationFilter认证的时候需要将认证信息封装到一个令牌实体中 SmsCodeAuthenticationToken最终实现认证的是认证提供商 SmsCodeAuthenticationProvider将自定义的过滤器添加到UsernamePasswordAuthenticationFilter过滤器的前面
二:自定义认证流程
pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.5.RELEASE
com.example
springboot-security-example
0.0.1-SNAPSHOT
springboot-security-example
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.1.2.RELEASE
org.springframework.security.oauth
spring-security-oauth2
2.3.4.RELEASE
compile
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
2.9.0
org.projectlombok
lombok
org.springframework.boot
spring-boot-starter-test
org.springframework.boot
spring-boot-maven-plugin
application.yml
server:
port: 8080
# Redis数据库索引(默认为0)
spring:
redis:
database: 0
host: localhost
port: 6379
password:
logging:
level:
org.springframework: debug
SmsCodeAuthenticationFilter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public SmsCodeAuthenticationFilter(String loginProcessUrlMobile) {
super(new AntPathRequestMatcher(loginProcessUrlMobile, "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
// 封装令牌
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 开始认证
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
String mobile = request.getParameter("mobile");
if (mobile == null) {
mobile = "";
}
return mobile.trim();
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
SmsCodeAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 1L;
/** 身份 */
private final Object principal;
public SmsCodeAuthenticationToken(Object mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
super.setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
@Override
public Object getCredentials() {
return null;
}
}
SmsCodeAuthenticationProvider
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
// TODO 在这里校验验证码是否正确,验证码一般存放到redis中
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
SmsCodeAuthenticationSecurityConfig
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter("/authentication/mobile");
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
SmsCodeAuthenticationProvider provider = new SmsCodeAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
// 将SmsCodeAuthenticationFilter放到过滤器链的UsernamePasswordAuthenticationFilter的后面
http
.authenticationProvider(provider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
MyAuthenticationSuccessHandler
/**
* 登录成功时执行
*
* @author Mengday Zhang
* @version 1.0
* @since 2019-04-20
*/
@Slf4j
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("login sucesssful {}", objectMapper.writeValueAsString(authentication));
String header = request.getHeader("Authorization");
if (header == null || !header.toLowerCase().startsWith("basic ")) {
throw new UnapprovedClientAuthenticationException("请求头中没有clientId");
}
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String clientId = tokens[0];
String clientSecret = tokens[1];
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("clientId配置信息不存在,clientId=" + clientId);
} else if (!new BCryptPasswordEncoder().matches(clientSecret, clientDetails.getClientSecret())) {
throw new UnapprovedClientAuthenticationException("clientSecret不匹配,clientId=" + clientId);
}
// grantType 为自定义的"custom"
TokenRequest tokenRequest = new TokenRequest(new HashMap(), clientId, clientDetails.getScope(), "custom");
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(accessToken));
}
/**
* Decodes the header into a username and password.
*
* @throws BadCredentialsException if the Basic header is not present or is not valid
* Base64
*/
private String[] extractAndDecodeHeader(String header, HttpServletRequest request)
throws IOException {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
}
catch (IllegalArgumentException e) {
throw new BadCredentialsException(
"Failed to decode basic authentication token");
}
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
}
}
MyAuthenticationFailureHandler
@Slf4j
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("认证失败");
response.setContentType("text/html;charset=utf-8");
response.getWriter().write(exception.getMessage());
}
}
SecurityConfiguration
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.apply(smsCodeAuthenticationSecurityConfig)
.and()
.formLogin()
.loginPage("/authentication/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler);
http.authorizeRequests()
.antMatchers("/login", "/authentication/form", "/authentication/mobile", "/code/sms").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
AuthorizationServerConfiguration
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 使用in-memory存储
.withClient("clientId")
.secret(new BCryptPasswordEncoder().encode("clientSecret"))
.authorizedGrantTypes("authorization_code")
.scopes("all")
.redirectUris("http://www.baidu.com");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
ResourceServerConfiguration
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.apply(smsCodeAuthenticationSecurityConfig)
.and()
.formLogin()
.loginPage("/authentication/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler);
http.authorizeRequests()
.antMatchers("/login", "/authentication/form", "/authentication/mobile", "/code/sms").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
SmsValidateCodeController
@RestController
public class SmsValidateCodeController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping(value = "/code/sms", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public SmsCode createCode(@RequestHeader("deviceId") String deviceId, String mobile) {
SmsCode smsCode = createSmsCode();
System.out.println("验证码发送成功:" + smsCode);
String key = "code:sms:"+ deviceId;
stringRedisTemplate.opsForValue().set(key, smsCode.getCode());
return smsCode;
}
private SmsCode createSmsCode() {
String code = (int) ((Math.random() * 9 + 1) * 100000) + "";
return new SmsCode(code, 30000);
}
}
三:获取token
1. 发送短信验证码
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/7f99b696cfdffe57c8b84c89b60d6905.png)
2. 获取token
1. 通过rest client 获取token
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/d4231f6fda9626dc956fecef4503705d.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/5363985a27d85cbf52e059725639128f.png)
2. 通过curl命令行获取token
通过上面的Copy as cURL按钮获取curl命令行请求,然后执行命令行获取token(在命令行上都能获取token,那么app上更能获取到token) ![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/953ec2edc278c2deeaa945d3aed0b733.png)
|