如今的好多項目都是基於APP移動端以及先後端分離的項目,以前基於Session的先後端放到一塊兒的項目已經慢慢失寵並淡出咱們視線,尤爲是當基於SpringCloud的微服務架構以及Vue、React單頁面應用流行起來後,狀況更甚。爲此基於先後端分離的項目用戶認證也受到衆人關注的一個焦點,不一樣以往的基於Session用戶認證,基於Token的用戶認證是目前主流選擇方案(至於什麼是Token認證,網上有相關的資料,你們能夠看看),並且基於Java的兩大認證框架有Apache Shiro和SpringSecurity,我在此就不討論孰優孰劣的,你們可自行百度看看,本文主要討論的是基於SpringSecurity的用戶認證。css
建立三個項目第一個項目awbeci-ssb是主項目包含兩個子項目awbeci-ssb-api和awbeci-ssb-core,而且引入相關SpringSecurity jar包,以下所示:
下面是個人項目目錄結構,代碼我會在最後放出來前端
資源服務通常是配置用戶名密碼或者手機號驗證碼、社交登陸等等用戶認證方式的配置以及一些靜態文件地址和相關請求地址設置要不要認證等等做用。java
認證服務是配置認證使用的方式,如Redis、JWT等等,還有一個就是設置ClientId和ClinetSecret,只有正確的ClientId和ClinetSecret才能獲取Token。git
3)首先咱們建立兩個類一個繼承AuthorizationServerConfigurerAdapter的SsbAuthorizationServerConfig做爲認證服務類和一個繼承ResourceServerConfigurerAdapter的SsbResourceServerConfig資源服務類,這兩個類實現好,大概已經完成50%了,代碼以下:github
@Configuration @EnableAuthorizationServer public class SsbAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired public SsbAuthorizationServerConfig(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.userDetailsService(userDetailsService); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory()//配置內存中,也能夠是數據庫 .withClient("awbeci")//clientid .secret("awbeci-secret") .accessTokenValiditySeconds(3600)//token有效時間 秒 .authorizedGrantTypes("refresh_token", "password", "authorization_code")//token模式 .scopes("all")//限制容許的權限配置 .and()//下面配置第二個應用 (不知道動態的是怎麼配置的,那就不能使用內存模式,應該使用數據庫模式來吧) .withClient("test") .scopes("testSc") .accessTokenValiditySeconds(7200) .scopes("all"); } }
@Configuration @EnableResourceServer public class SsbResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired protected AuthenticationSuccessHandler ssbAuthenticationSuccessHandler; @Autowired protected AuthenticationFailureHandler ssbAuthenticationFailureHandler; @Autowired private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; @Override public void configure(HttpSecurity http) throws Exception { // 因此在咱們的app登陸的時候咱們只要提交的action,不要跳轉到登陸頁 http.formLogin() //登陸頁面,app用不到 //.loginPage("/authentication/login") //登陸提交action,app會用到 // 用戶名登陸地址 .loginProcessingUrl("/form/token") //成功處理器 返回Token .successHandler(ssbAuthenticationSuccessHandler) //失敗處理器 .failureHandler(ssbAuthenticationFailureHandler); http // 手機驗證碼登陸 .apply(smsCodeAuthenticationSecurityConfig) .and() .authorizeRequests() //手機驗證碼登陸地址 .antMatchers("/mobile/token", "/email/token") .permitAll() .and() .authorizeRequests() .antMatchers( "/register", "/social/**", "/**/*.js", "/**/*.css", "/**/*.jpg", "/**/*.png", "/**/*.woff2", "/code/image") .permitAll()//以上的請求都不須要認證 .anyRequest() .authenticated() .and() .csrf().disable(); } }
配置好以後,下面我能夠正式開始使用SpringSecurity OAuth配置用戶名和密碼登陸,也就是表單登陸,SpringSecurity默認有Form登陸和Basic登陸,咱們已經在SsbResourceServerConfig類的configure方法上面設置了 http.formLogin()也就是表單登陸,也就是這裏的用戶名密碼登陸,默認狀況下SpringSecurity已經實現了表單登陸的封裝了,因此咱們只要設置成功以後返回的Token就好,咱們建立一個繼承SavedRequestAwareAuthenticationSuccessHandler的SsbAuthenticationSuccessHandler類,代碼以下:web
@Component("ssbAuthenticationSuccessHandler") public class SsbAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Autowired private ObjectMapper objectMapper; @Autowired private ClientDetailsService clientDetailsService; @Autowired private AuthorizationServerTokenServices authorizationServerTokenServices; /* * (non-Javadoc) * * @see org.springframework.security.web.authentication. * AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http. * HttpServletRequest, javax.servlet.http.HttpServletResponse, * org.springframework.security.core.Authentication) */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String header = request.getHeader("Authorization"); String name = authentication.getName(); // String password = (String) authentication.getCredentials(); if (header == null || !header.startsWith("Basic ")) { throw new UnapprovedClientAuthenticationException("請求頭中無client信息"); } 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); } else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) { throw new UnapprovedClientAuthenticationException("clientSecret不匹配:" + clientId); } TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom"); OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails); OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication); OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(token)); } private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException { byte[] base64Token = header.substring(6).getBytes("UTF-8"); byte[] decoded; try { decoded = Base64.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) }; } }
這樣就能夠成功的返回Token給前端,而後咱們必須放開/form/token請求地址,咱們已經在SsbResourceServerConfig類的configure方法放行了,而且設置成功處理類ssbAuthenticationSuccessHandler方法,和失敗處理類ssbAuthenticationFailureHandler以下所示:redis
下面咱們就用PostMan測試下看是否成功,不過在這以前咱們還要建立一個基於UserDetailsService的ApiUserDetailsService類,這個類的使用是從數據庫中查詢認證的用戶信息,這裏咱們就沒有從數據庫中查詢,可是你要知道這個類是作什麼用的,代碼以下:spring
@Component public class ApiUserDetailsService implements UserDetailsService{ private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private PasswordEncoder passwordEncoder; /* * (non-Javadoc) * * @see org.springframework.security.core.userdetails.UserDetailsService# * loadUserByUsername(java.lang.String) */ // 這裏的username 能夠是username、mobile、email public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("表單登陸用戶名:" + username); return buildUser(username); } private SocialUser buildUser(String userId) { // 根據用戶名查找用戶信息 //根據查找到的用戶信息判斷用戶是否被凍結 String password = passwordEncoder.encode("123456"); logger.info("數據庫密碼是:" + password); return new SocialUser(userId, password, true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_USER")); } }
這樣用戶名密碼登陸就成功了!下面咱們來處理手機號驗證碼登陸獲取token。數據庫
首先要配置redis,咱們把驗證碼放到redis裏面(注意,發送驗證碼其實就是往redis裏面保存一條記錄,這個我就不詳細說了),配置以下所示:json
spring.redis.host=127.0.0.1 spring.redis.password=zhangwei spring.redis.port=6379 # 鏈接超時時間(毫秒) spring.redis.timeout=30000
設置好以後,咱們要建立四個類
1.基於AbstractAuthenticationToken的SmsCodeAuthenticationToken類,存放token用戶信息類
2.基於AbstractAuthenticationProcessingFilter的SmsCodeAuthenticationFilter類,這是個過濾器,把請求的參數如手機號、驗證碼獲取到,並構造Authentication
3.基於AuthenticationProvider的SmsCodeAuthenticationProvider類,這個類就是驗證你手機號和驗證碼是否正確,並返回Authentication
4.基於SecurityConfigurerAdapter的SmsCodeAuthenticationSecurityConfig類,這個類是承上啓下的使用,把上面三個類配置到這裏面並放到資源服務裏面讓它起使用
下面咱們來一個一個解析這四個類。
(1)、SmsCodeAuthenticationToken類,代碼以下 :
// 用戶基本信息存儲類 public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken{ // 用戶信息所有放在這裏面,如用戶名,手機號,密碼等 private final Object principal; //這裏保存的證書信息,如密碼,驗證碼等 private Object credentials; //構造未認證以前用戶信息 SmsCodeAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); } //構造已認證用戶信息 SmsCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); // must use super, as we override } public Object getCredentials() { return this.credentials; } public Object getPrincipal() { return this.principal; } 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(); } }
(2)、SmsCodeAuthenticationFilter類,代碼以下
//短信驗證碼攔截器 public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private boolean postOnly = true; // 手機號參數變量 private String mobileParameter = "mobile"; private String smsCode = "smsCode"; SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/mobile/token", "POST")); } /** * 添加未認證用戶認證信息,而後在provider裏面進行正式認證 * * @param httpServletRequest * @param httpServletResponse * @return * @throws AuthenticationException * @throws IOException * @throws ServletException */ public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException { if (postOnly && !httpServletRequest.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + httpServletRequest.getMethod()); } String mobile = obtainMobile(httpServletRequest); String smsCode = obtainSmsCode(httpServletRequest); //todo:驗證短信驗證碼2 if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, smsCode); // Allow subclasses to set the "details" property setDetails(httpServletRequest, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } /** * 獲取手機號 */ private String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } private String obtainSmsCode(HttpServletRequest request) { return request.getParameter(smsCode); } private void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public void setMobileParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.mobileParameter = usernameParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return mobileParameter; } }
(3)、SmsCodeAuthenticationProvider類,代碼以下
//用戶認證所在類 public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private RedisTemplate<Object, Object> redisTemplate; // 注意這裏的userdetailservice ,由於SmsCodeAuthenticationProvider類沒有@Component // 因此這裏不能加@Autowire,只能經過外面設置才行 private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 在這裏認證用戶信息 * @param authentication * @return * @throws AuthenticationException */ public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; // String mobile = (String) authenticationToken.getPrincipal(); String mobile = authentication.getName(); String smsCode = (String) authenticationToken.getCredentials(); //從redis中獲取該手機號的驗證碼 String smsCodeFromRedis = (String) redisTemplate.opsForValue().get(mobile); if(!smsCode.equals(smsCodeFromRedis)){ throw new InternalAuthenticationServiceException("手機驗證碼不正確"); } UserDetails user = userDetailsService.loadUserByUsername(mobile); if (user == null) { throw new InternalAuthenticationServiceException("沒法獲取用戶信息"); } SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user,null, user.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } public RedisTemplate<Object, Object> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) { this.redisTemplate = redisTemplate; } }
(4)、SmsCodeAuthenticationSecurityConfig類,代碼以下
@Component public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private AuthenticationSuccessHandler ssbAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler ssbAuthenticationFailureHandler; @Autowired private UserDetailsService userDetailsService; @Autowired private RedisTemplate<Object, Object> redisTemplate; @Override public void configure(HttpSecurity http) throws Exception { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(ssbAuthenticationSuccessHandler); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(ssbAuthenticationFailureHandler); SmsCodeAuthenticationProvider smsCodeDaoAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeDaoAuthenticationProvider.setUserDetailsService(userDetailsService); smsCodeDaoAuthenticationProvider.setRedisTemplate(redisTemplate); http.authenticationProvider(smsCodeDaoAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
上面 代碼都有註解我就不詳細講了,好了咱們再來測試下看看是否成功:
好了,手機號驗證碼用戶認證也成功了!
郵箱驗證碼登陸和上面手機號驗證碼登陸差很少,大家本身試着寫一下。
這是拓展功能,不須要的同窗能夠忽略。
咱們改造一下SsbAuthorizationServerConfig類,以支持Redis保存token,以下
@Autowired private TokenStore redisTokenStore; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //使用Redis做爲Token的存儲 endpoints .tokenStore(redisTokenStore) .userDetailsService(userDetailsService); }
而後再新建一下RedisTokenStoreConfig類
@Configuration @ConditionalOnProperty(prefix = "ssb.security.oauth2", name = "storeType", havingValue = "redis") public class RedisTokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore(){ return new RedisTokenStore(redisConnectionFactory); } }
在application.properties裏面添加
ssb.security.oauth2.storeType=redis
好了,咱們測試下
這樣就成功的保存到redis了。
jwt是什麼請自行百度。
首先仍是要改造SsbAuthorizationServerConfig類,代碼以下:
@Autowired(required = false) private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired(required = false) private TokenEnhancer jwtTokenEnhancer; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //使用Redis做爲Token的存儲 endpoints // .tokenStore(redisTokenStore) // .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); //一、設置token爲jwt形式 //二、設置jwt 拓展認證信息 if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancers = new ArrayList<TokenEnhancer>(); enhancers.add(jwtTokenEnhancer); enhancers.add(jwtAccessTokenConverter); enhancerChain.setTokenEnhancers(enhancers); endpoints.tokenEnhancer(enhancerChain) .accessTokenConverter(jwtAccessTokenConverter); } }
而後咱們再來建立JwtTokenStoreConfig類代碼以下:
@Configuration @ConditionalOnProperty( prefix = "ssb.security.oauth2", name = "storeType", havingValue = "jwt", matchIfMissing = true) public class JwtTokenStoreConfig { @Value("${ssb.security.jwt.signingKey}") private String signingkey; @Bean public TokenEnhancer jwtTokenEnhancer() { return new SsbJwtTokenEnhancer(); } @Bean public TokenStore jetTokenStroe() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); //設置默認值 if(StringUtils.isEmpty(signingkey)){ signingkey = "awbeci"; } //密鑰,放到配置文件中 jwtAccessTokenConverter.setSigningKey(signingkey); return jwtAccessTokenConverter; } }
再建立一個基於JwtTokenEnhancerHandler的ApiJwtTokenEnhancerHandler類,代碼以下:
/** * 拓展jwt token裏面的信息 */ @Service public class ApiJwtTokenEnhancerHandler implements JwtTokenEnhancerHandler { public HashMap<String, Object> getInfoToToken() { HashMap<String, Object> info = new HashMap<String, Object>(); info.put("author", "張威"); info.put("company", "awbeci-copy"); return info; } }
最後不要忘了在application.properties裏面設置一下
ssb.security.oauth2.storeType=jwt ssb.security.jwt.signingKey=awbeci
好了,咱們來測試一下吧
1)spring-security已經幫咱們封裝了用戶名密碼的表單登陸了,咱們只要實現手機號驗證碼登陸就好
2)一共6個類,一個資源服務類ResourceServerConfigurer,一個認證服務類 AuthorizationServerConfigurer,一個手機驗證碼Token類,一個手機驗證碼Filter類,一個認證手機驗證碼類Provider類,一個配置類Configure類,就這麼多,其實不難,有時候看網上人家寫的好多,看着都要嚇死。
3)後面有時間寫一下SSO單點登陸的文章
4)源碼