一:简介
通过短信验证码获取令牌的过程也是自定义认证的过程。 组定义认证流程如下:
- 自定义一个过滤器实现认证 SmsCodeAuthenticationFilter
- 认证的时候需要将认证信息封装到一个令牌实体中 SmsCodeAuthenticationToken
- 最终实现认证的是认证提供商 SmsCodeAuthenticationProvider
- 将自定义的过滤器添加到UsernamePasswordAuthenticationFilter过滤器的前面
二:自定义认证流程
- pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-security-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-security-example</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 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<? extends GrantedAuthority> 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<DefaultSecurityFilterChain, HttpSecurity> {
@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. 发送短信验证码
2. 获取token
1. 通过rest client 获取token
2. 通过curl命令行获取token
通过上面的Copy as cURL按钮获取curl命令行请求,然后执行命令行获取token(在命令行上都能获取token,那么app上更能获取到token)