摘要:前言現在的好多項目都是基于移動端以及前后端分離的項目,之前基于的前后端放到一起的項目已經慢慢失寵并淡出我們視線,尤其是當基于的微服務架構以及單頁面應用流行起來后,情況更甚。使用生成是什么請自行百度。
1、前言
現在的好多項目都是基于APP移動端以及前后端分離的項目,之前基于Session的前后端放到一起的項目已經慢慢失寵并淡出我們視線,尤其是當基于SpringCloud的微服務架構以及Vue、React單頁面應用流行起來后,情況更甚。為此基于前后端分離的項目用戶認證也受到眾人關注的一個焦點,不同以往的基于Session用戶認證,基于Token的用戶認證是目前主流選擇方案(至于什么是Token認證,網上有相關的資料,大家可以看看),而且基于Java的兩大認證框架有Apache Shiro和SpringSecurity,我在此就不討論孰優孰劣的,大家可自行百度看看,本文主要討論的是基于SpringSecurity的用戶認證。
2、準備工作創建三個項目第一個項目awbeci-ssb是主項目包含兩個子項目awbeci-ssb-api和awbeci-ssb-core,并且引入相關SpringSecurity jar包,如下所示:
下面是我的項目目錄結構,代碼我會在最后放出來
資源服務一般是配置用戶名密碼或者手機號驗證碼、社交登錄等等用戶認證方式的配置以及一些靜態文件地址和相關請求地址設置要不要認證等等作用。
認證服務是配置認證使用的方式,如Redis、JWT等等,還有一個就是設置ClientId和ClinetSecret,只有正確的ClientId和ClinetSecret才能獲取Token。
3)首先我們創建兩個類一個繼承AuthorizationServerConfigurerAdapter的SsbAuthorizationServerConfig作為認證服務類和一個繼承ResourceServerConfigurerAdapter的SsbResourceServerConfig資源服務類,這兩個類實現好,大概已經完成50%了,代碼如下:
@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(); } }4、用戶名密碼登錄獲取Token
配置好之后,下面我可以正式開始使用SpringSecurity OAuth配置用戶名和密碼登錄,也就是表單登錄,SpringSecurity默認有Form登錄和Basic登錄,我們已經在SsbResourceServerConfig類的configure方法上面設置了 http.formLogin()也就是表單登錄,也就是這里的用戶名密碼登錄,默認情況下SpringSecurity已經實現了表單登錄的封裝了,所以我們只要設置成功之后返回的Token就好,我們創建一個繼承SavedRequestAwareAuthenticationSuccessHandler的SsbAuthenticationSuccessHandler類,代碼如下:
@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如下所示:
下面我們就用PostMan測試下看是否成功,不過在這之前我們還要創建一個基于UserDetailsService的ApiUserDetailsService類,這個類的使用是從數據庫中查詢認證的用戶信息,這里我們就沒有從數據庫中查詢,但是你要知道這個類是做什么用的,代碼如下:
@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。
5、手機號驗證碼登錄獲取Token首先要配置redis,我們把驗證碼放到redis里面(注意,發送驗證碼其實就是往redis里面保存一條記錄,這個我就不詳細說了),配置如下所示:
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
(4)、SmsCodeAuthenticationSecurityConfig類,代碼如下
@Component public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter{ @Autowired private AuthenticationSuccessHandler ssbAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler ssbAuthenticationFailureHandler; @Autowired private UserDetailsService userDetailsService; @Autowired private RedisTemplate 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); } }
上面 代碼都有注解我就不詳細講了,好了我們再來測試下看看是否成功:
好了,手機號驗證碼用戶認證也成功了!
郵箱驗證碼登錄和上面手機號驗證碼登錄差不多,你們自己試著寫一下。
7、將token保存到Redis里面這是拓展功能,不需要的同學可以忽略。
我們改造一下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了。
8、使用JWT生成Tokenjwt是什么請自行百度。
首先還是要改造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); //1、設置token為jwt形式 //2、設置jwt 拓展認證信息 if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); Listenhancers = new ArrayList (); 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 HashMapgetInfoToToken() { HashMap info = new HashMap (); info.put("author", "張威"); info.put("company", "awbeci-copy"); return info; } }
最后不要忘了在application.properties里面設置一下
ssb.security.oauth2.storeType=jwt ssb.security.jwt.signingKey=awbeci
好了,我們來測試一下吧
9、總結1)spring-security已經幫我們封裝了用戶名密碼的表單登錄了,我們只要實現手機號驗證碼登錄就好
2)一共6個類,一個資源服務類ResourceServerConfigurer,一個認證服務類 AuthorizationServerConfigurer,一個手機驗證碼Token類,一個手機驗證碼Filter類,一個認證手機驗證碼類Provider類,一個配置類Configure類,就這么多,其實不難,有時候看網上人家寫的好多,看著都要嚇死。
3)后面有時間寫一下SSO單點登錄的文章
4)源碼
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77379.html
摘要:我們以微信為例,首先我們發送一個請求,因為你已經登錄了,所以后臺可以獲取當前是誰,然后就獲取到請求的鏈接,最后就是跳轉到這個鏈接上面去。 1、準備工作 申請QQ、微信相關AppId和AppSecret,這些大家自己到QQ互聯和微信開發平臺 去申請吧 還有java后臺要引入相關的jar包,如下: org.springframework.security....
摘要:開公眾號差不多兩年了,有不少原創教程,當原創越來越多時,大家搜索起來就很不方便,因此做了一個索引幫助大家快速找到需要的文章系列處理登錄請求前后端分離一使用完美處理權限問題前后端分離二使用完美處理權限問題前后端分離三中密碼加鹽與中異常統一處理 開公眾號差不多兩年了,有不少原創教程,當原創越來越多時,大家搜索起來就很不方便,因此做了一個索引幫助大家快速找到需要的文章! Spring Boo...
摘要:前言基于做微服務架構分布式系統時,作為認證的業內標準,也提供了全套的解決方案來支持在環境下使用,提供了開箱即用的組件。 前言 基于SpringCloud做微服務架構分布式系統時,OAuth2.0作為認證的業內標準,Spring Security OAuth2也提供了全套的解決方案來支持在Spring Cloud/Spring Boot環境下使用OAuth2.0,提供了開箱即用的組件。但...
閱讀 1074·2021-11-24 09:39
閱讀 1307·2021-11-18 13:18
閱讀 2425·2021-11-15 11:38
閱讀 1824·2021-09-26 09:47
閱讀 1625·2021-09-22 15:09
閱讀 1624·2021-09-03 10:29
閱讀 1510·2019-08-29 17:28
閱讀 2951·2019-08-29 16:30