Oauth2.0是什么不在贅述,本文主要介紹如何使用SpringSecurity Oauth2.0實現自定義的用戶校驗
1.鑒權中心服務
首先,列舉一下我們需要用到的依賴,本文采用的是數據庫保存用戶信息redis保存token的方式。
pom依賴:
---- security依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.3.4.RELEASE</version> </dependency>
---- oauth2.0依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.2.4.RELEASE</version> </dependency>
----- redis依賴用於存儲token
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.3.4.RELEASE</version> </dependency>
----- mybatisPlus用於操作數據庫
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.0</version> </dependency>
------mysql 驅動 <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency>
添加完引用后,我們需要配置Oauth鑒權中的兩大服務:鑒權服務和資源服務
一、鑒權服務
鑒權服務中我們需要配置存儲token用的redis,用於從DB中拉取Client信息的服務類,用於進行Token生產和維護的服務類,最后還要外露獲取token的節點和進行token校驗的節點
在開始以前首先我們要搞清SpringSecurity中的一些基本類的作用和概念,這里貼出一篇不錯的文章: https://www.cnkirito.moe/spring-security-1/
1.首先我們要定義我們的用戶實體類,必須要實現UserDetails接口,我們的實體類中僅僅簡單的定義了用戶名和密碼,剩下的全部實現自UserDetails接口
public class User implements UserDetails { @TableId(type = IdType.AUTO) private int id; private String username; private String password; private boolean isEnabled; public int getId() { return id; } public void setId(int id) { this.id = id; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setEnabled(boolean enabled) { isEnabled = enabled; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.isEnabled; } }
數據庫腳本:
CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(50) NOT NULL, `password` varchar(255) NOT NULL, `is_account_non_expired` tinyint(4) NOT NULL, `is_account_non_locked` tinyint(4) DEFAULT NULL, `is_credentials_non_expired` tinyint(4) NOT NULL, `is_enabled` tinyint(4) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
2.自定義自己的用戶查找服務類,必須集成UserDetaailsService接口且實現loadUserByUsername接口,屆時spring框架會通過這個方法內的自定義邏輯找到你想要的用戶
public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user= userMapper.queryUserByUsername(username); if (user==null){ throw new UsernameNotFoundException("user no found"); } return user; } }
userMapper:就是一個非常基礎的數據庫查詢語句
<mapper namespace="com.example.oauth2.mapper.UserMapper"> <select id="queryUserByUsername" resultType="com.example.oauth2.entity.User"> select * from t_user where user_name=#{username} </select> </mapper>
3.配置鑒權服務器,我們需要把redis和上面自定義的用戶查詢配置到鑒權服務里
Configuration //開啟鑒權服務器 @EnableAuthorizationServer public class AuthoriztionServerConfiguration extends AuthorizationServerConfigurerAdapter { //全局SpringSecurity AuthenticationManager 需要在SpringSecurity中注入,下文會寫到 @Autowired private AuthenticationManager authenticationManager; //redis工廠會自動配置 @Autowired private RedisConnectionFactory redisConnectionFactory; //數據源會自動配置 @Autowired private DataSource dataSource; //我們自定義的UserDetailsService實現類 @Autowired private UserDetailsService userDetailsService; //全局加密編碼類 @Autowired private BCryptPasswordEncoder passwordEncoder; /** * 用來配置令牌端點的安全約束 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security //允許所有用戶訪問 /oauth/token節點來獲取token .tokenKeyAccess("permitAll()") //允許已經鑒權通過的用戶訪問 /oauth/check_token節點 .checkTokenAccess("isAuthenticated()") // 允許表單認證 .allowFormAuthenticationForClients(); } /** * 定義客戶端詳細信息的配置器 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 從DB中拉取client信息 clients.withClientDetails(jdbcClientDetailsService()); } /** *配置鑒權服務終結點配置 * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{ endpoints.authenticationManager(this.authenticationManager) //配置允許訪問的http方式 .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST) //用於配置自定義的用戶校驗服務 .userDetailsService(userDetailsService) //配置自定義的token保存地址 .tokenStore(tokenStore()) //用於配置自定義的token維護服務 .tokenServices(tokenServices()); } /** * 配置使用redis來存儲token * @return */ @Bean public TokenStore tokenStore(){ return new RedisTokenStore(redisConnectionFactory); } /** * 配置通過數據的方式去讀取client信息 * @return */ @Bean public ClientDetailsService jdbcClientDetailsService(){ JdbcClientDetailsService jdbcClientDetailsService=new JdbcClientDetailsService(dataSource); jdbcClientDetailsService.setPasswordEncoder(passwordEncoder); return jdbcClientDetailsService; } /** * 自定義token的生產和過期機制 * @return */ @Bean public DefaultTokenServices tokenServices(){ DefaultTokenServices defaultTokenServices=new DefaultTokenServices(); defaultTokenServices.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(4)); defaultTokenServices.setRefreshTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(2)); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setTokenStore(tokenStore()); return defaultTokenServices; } /** * 指定全局加密方式 * @return */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } }
同時我們需要在數據庫中建一張client的信息表SQL腳本:
CREATE TABLE `oauth_client_details` ( `client_id` varchar(256) NOT NULL, `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `authorized_grant_types` varchar(256) DEFAULT NULL, `web_server_redirect_uri` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
完成以上步驟后鑒權服務器就配置完成了,記下來我們需要配置資源服務器
2.資源服務器
資源服務器配置相對簡單,我們只需要暴露出可供用戶使用的節點即可
@Configuration //開啟資源服務 @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception{ http .authorizeRequests() //暴露出/oauth/**的所有接口 .antMatchers("/oauth/**") .permitAll() .anyRequest() .authenticated(); } }
配置完資源服務之后最關鍵的一步來了,我們需要自定義自己的用戶校驗
3.自定義用戶校驗
SpringSecurity中所有的校驗都是通過AuthenticationProvider來實現的,所以我們需要實現自己的Provider注入到用於校驗的Provider列表中去
@Component public class MyAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { //前面我們自己實現的讀取用戶信息的實現類 @Autowired private UserDetailsService userDetailsService; //全局加密方式 @Autowired private BCryptPasswordEncoder passwordEncoder; //對表單傳入的用戶信息和線程上下文中的用戶進行比對判斷是否是正確的用戶 @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException { //數據庫中的密碼 String authPassword = userDetails.getPassword(); //上下文中的密碼 String tokenPassword = (String) usernamePasswordAuthenticationToken.getCredentials(); boolean isPass = passwordEncoder.matches(tokenPassword, authPassword); if (isPass) { return; } throw new AuthenticationServiceException("password.wrong"); } //根據表單中傳入的用戶名從數據庫中獲取用戶信息 @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException { UserDetails user = userDetailsService.loadUserByUsername(username); if (user == null) { throw new AuthenticationServiceException("未找到用戶"); } return user; } }
把我們自定的provider注入進去
@Configuration public class GlobalAuthenticationConfig extends GlobalAuthenticationConfigurerAdapter { @Autowired private MyAuthenticationProvider authenticationProvider; @Override public void init(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider( authenticationProvider); } }
最后我們需要配置一下SpringSecurity
4.配置SpringSecurity
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationProvider authenticationProvider; /** * 注入用戶信息服務 * @return 用戶信息服務對象 */ @Bean @Override public UserDetailsService userDetailsService() { return new UserDetailsServiceImpl(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(HttpMethod.OPTIONS,"/oauth/**") .permitAll() .anyRequest() .authenticated() .and() .csrf() .disable(); } }
然后配置文件中配好數據庫和redis信息
server: port: 5000 spring: datasource: url: xxx username: xxx password: xxx driver-class-name: com.mysql.jdbc.Driver redis: host: xxx port: 6379 database: 7 password: xxx mybatis-plus: mapper-locations: classpath:/mapper/*Mapper.xml configuration: map-underscore-to-camel-case: true logging: level: ROOT: debug
在啟動之前我們需要先在數據庫中添加一個client和一個用戶
@SpringBootTest class Oauth2ApplicationTests { @Autowired JdbcClientDetailsService jdbcClientDetailsService; @Autowired private BCryptPasswordEncoder passwordEncoder; @Test void contextLoads() { //通過我們在鑒權服務中注入的JdbcClientDetailsService來插入一條Client信息 BaseClientDetails baseClientDetails = new BaseClientDetails(); baseClientDetails.setClientId("client"); baseClientDetails.setClientSecret(passwordEncoder.encode("secret")); List<String> grantTypes = new ArrayList<>(); grantTypes.add("password"); baseClientDetails.setAuthorizedGrantTypes(grantTypes); jdbcClientDetailsService.addClientDetails(baseClientDetails); //用指定的密碼生成器搞一個密碼一會手動填到庫里這里就不寫sql 了 String str = passwordEncoder.encode("666"); System.out.println("終於他媽的完事了");/**/ } }
注意給scope填個select上面忘記寫了
啟動項目訪問獲取通通token的接口,完美。