近期一直在研究鑒權方面的各種案例,這幾天有空,寫一波總結及經驗。
第一步:什么是 OAuth鑒權
OAuth2是工業標准的授權協議。OAuth2取代了在2006創建的原始OAuthTM協議所做的工作。OAuth 2.0關注客戶端開發人員的簡單性,同時為Web應用程序、桌面應用程序、移動電話和客廳設備提供特定的授權流。
參考理解: Oauth2.0 理解OAuth 2.0 QQ授權 微信授權
第二步:什么是授權碼模式
授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的后台服務器,與"服務提供商"的認證服務器進行互動。
第三步:授權碼授權流程
交互步驟:
(A)用戶訪問客戶端,后者將前者導向認證服務器。
(B)用戶選擇是否給予客戶端授權。
(C)假設用戶給予授權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。 (D)客戶端收到授權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的后台的服務器上完成的,對用戶不可見。 (E)認證服務器核對了授權碼和重定向URI,確認無誤后,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。
第四步:授權碼模式實踐
源碼地址:
github: https://github.com/GitHubZhangCom/spring-security-oauth-example/
碼雲:https://gitee.com/region/spring-security-oauth-example/
1)、 引入相關jar:
<!--security --> <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.0.0.RELEASE</version> </dependency> <!-- 使用lombok優雅的編碼 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
2)、相關oauth編程:
package com.auth.server.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; /** * 鑒權配置 * @author zyl * */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Qualifier("myUserDetailsService") @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/**/*.js", "/**/*.css" ) .permitAll() .anyRequest() .authenticated() .and() .formLogin() .loginPage("/login") .permitAll() // 自動登錄 /*.and() .rememberMe() // 加密的秘鑰 .key("unique-and-secret") // 存放在瀏覽器端cookie的key .rememberMeCookieName("remember-me-cookie-name") // token失效的時間,單位為秒 .tokenValiditySeconds(60 * 60 * 25)*/ .and() // 暫時禁用CSRF,否則無法提交登錄表單 .csrf().disable(); } }
package com.auth.server.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; 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.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; /** * 授權配置 * @author wb0024 * */ @Configuration @EnableAuthorizationServer public class ServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Qualifier("myUserDetailsService") @Autowired private UserDetailsService userDetailsService; // @Autowired // @Qualifier("dataSource") // private DataSource dataSource; @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { // 配置token獲取和驗證時的策略 security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client") // secret密碼配置從 Spring Security 5.0開始必須以 {加密方式}+加密后的密碼 這種格式填寫 /* * 當前版本5新增支持加密方式: * bcrypt - BCryptPasswordEncoder (Also used for encoding) * ldap - LdapShaPasswordEncoder * MD4 - Md4PasswordEncoder * MD5 - new MessageDigestPasswordEncoder("MD5") * noop - NoOpPasswordEncoder * pbkdf2 - Pbkdf2PasswordEncoder * scrypt - SCryptPasswordEncoder * SHA-1 - new MessageDigestPasswordEncoder("SHA-1") * SHA-256 - new MessageDigestPasswordEncoder("SHA-256") * sha256 - StandardPasswordEncoder*/ .secret("{noop}secret") .scopes("all") .authorizedGrantTypes("authorization_code")//授權碼模式 // .authorizedGrantTypes("authorization_code", "refresh_token")//授權碼模式 .autoApprove(true); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { // // 配置tokenStore,保存到redis緩存中 // endpoints.authenticationManager(authenticationManager) // .tokenStore(new MyRedisTokenStore(redisConnectionFactory)) // // 不添加userDetailsService,刷新access_token時會報錯 // .userDetailsService(userDetailsService); // 使用最基本的InMemoryTokenStore生成token endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore()); } // 使用最基本的InMemoryTokenStore生成token @Bean public TokenStore memoryTokenStore() { return new InMemoryTokenStore(); } }
package com.jwt.server.provider; import java.util.ArrayList; 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.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** * 自定義身份認證驗證組件 * @author zyl * */ public class CustomAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; private BCryptPasswordEncoder bCryptPasswordEncoder; public CustomAuthenticationProvider(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder){ this.userDetailsService = userDetailsService; this.bCryptPasswordEncoder = bCryptPasswordEncoder; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 獲取認證的用戶名 & 密碼 String name = authentication.getName(); String password = authentication.getCredentials().toString(); // 認證邏輯 UserDetails userDetails = userDetailsService.loadUserByUsername(name); if (null != userDetails) { if (bCryptPasswordEncoder.matches(password, userDetails.getPassword())) { // 這里設置權限和角色 ArrayList<GrantedAuthority> authorities = new ArrayList<>(); authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN")); authorities.add( new GrantedAuthorityImpl("ROLE_API")); authorities.add( new GrantedAuthorityImpl("AUTH_WRITE")); // 生成令牌 這里令牌里面存入了:name,password,authorities, 當然你也可以放其他內容 Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities); return auth; } else { throw new BadCredentialsException("密碼錯誤"); } } else { throw new UsernameNotFoundException("用戶不存在"); } } /** * 是否可以提供輸入類型的認證服務 * @param authentication * @return */ @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
核心代碼需要做的就這些,詳情請看源碼。