一,oauth2的用途?
1,什么是oauth2?
OAuth2 是一個開放標准,
它允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密資源(如頭像、照片、視頻等),
在這個過程中無須將用戶名和密碼提供給第三方應用。
實現這一功能是通過提供一個令牌(token),而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據
2,spring 為oauth2提供的官方文檔:
https://projects.spring.io/spring-security-oauth/docs/oauth2.html
3,獲取令牌的方式主要有四種,分別是:
授權碼模式
簡單模式
密碼模式
客戶端模式
我們這里演示的是密碼模式
說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest
對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/
說明:作者:劉宏締 郵箱: 371125307@qq.com
二,演示項目的相關信息
1,項目地址:
https://github.com/liuhongdi/securityoauth2
2,項目功能說明:
演示了得到token,用token訪問資源等功能
3,項目結構:如圖:
三,配置文件說明
1,pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--oauth2--> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.5.0.RELEASE</version> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--jaxb--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!--mysql mybatis begin--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--fastjson begin--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.73</version> </dependency>
2,application.properties
#redis spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.database=0 spring.redis.password=lhddemo #mysql spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf8&useSSL=false spring.datasource.username=root spring.datasource.password=lhddemo spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #mybatis mybatis.mapper-locations=classpath:/mapper/*Mapper.xml mybatis.type-aliases-package=com.example.demo.mapper #error server.error.include-stacktrace=always #log logging.level.org.springframework.web=trace
3,數據庫:
建表sql:
CREATE TABLE `sys_user` ( `userId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `userName` varchar(100) NOT NULL DEFAULT '' COMMENT '用戶名', `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密碼', `nickName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵稱', PRIMARY KEY (`userId`), UNIQUE KEY `userName` (`userName`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶表'
INSERT INTO `sys_user` (`userId`, `userName`, `password`, `nickName`) VALUES (1, 'lhd', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '老劉'), (2, 'admin', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '管理員'), (3, 'merchant', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '商戶老張');
說明:3個密碼都是111111,僅供演示使用
CREATE TABLE `sys_user_role` ( `urId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `userId` int(11) NOT NULL DEFAULT '0' COMMENT '用戶id', `roleName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色id', PRIMARY KEY (`urId`), UNIQUE KEY `userId` (`userId`,`roleName`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶角色關聯表'
INSERT INTO `sys_user_role` (`urId`, `userId`, `roleName`) VALUES (1, 2, 'ADMIN'), (2, 3, 'MERCHANT');
四,java代碼說明
1,WebSecurityConfig.java
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final static BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder(); @Resource private SecUserDetailService secUserDetailService; //用戶信息類,用來得到UserDetails @Bean @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } @Bean @Override protected UserDetailsService userDetailsService() { return super.userDetailsService(); } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/oauth/**") .authorizeRequests() .antMatchers("/oauth/**").permitAll() .and().csrf().disable(); } @Resource public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(secUserDetailService).passwordEncoder(new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return ENCODER.encode(charSequence); } //密碼匹配,看輸入的密碼經過加密與數據庫中存放的是否一樣 @Override public boolean matches(CharSequence charSequence, String s) { return ENCODER.matches(charSequence,s); } }); } }
放開了到授權服務地址的訪問
2,AuthorizationServiceConfig.java
@Configuration @EnableAuthorizationServer public class AuthorizationServiceConfig extends AuthorizationServerConfigurerAdapter { @Resource AuthenticationManager authenticationManager; @Resource RedisConnectionFactory redisConnectionFactory; @Resource UserDetailsService userDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { String clientId = "client_id"; String clientSecret = "123"; clients.inMemory() //這個好比賬號 .withClient(clientId) //授權同意的類型 .authorizedGrantTypes("password", "refresh_token") //有效時間 .accessTokenValiditySeconds(1800) .refreshTokenValiditySeconds(60 * 60 * 2) .resourceIds("rid") //作用域,范圍 .scopes("all") //密碼 .secret(new BCryptPasswordEncoder().encode(clientSecret)); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory)) //身份驗證管理 .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //允許客戶端表單身份驗證 security.allowFormAuthenticationForClients(); } }
授權服務器的配置,允許client_id這個賬號的訪問
3,ResourceServiceConfig.java
@Configuration @EnableResourceServer public class ResourceServiceConfig extends ResourceServerConfigurerAdapter { @Resource private UserAuthenticationEntryPoint userAuthenticationEntryPoint; @Resource private UserAccessDeniedHandler userAccessDeniedHandler; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("rid"); } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/home/**").permitAll(); http.authorizeRequests() .antMatchers("/admin/**").hasAnyRole("admin","ADMIN") .antMatchers("/user/**").hasRole("user") .anyRequest().authenticated(); //access deny http.exceptionHandling().accessDeniedHandler(userAccessDeniedHandler); //unauthorized http.exceptionHandling().authenticationEntryPoint(userAuthenticationEntryPoint); } }
資源服務器的配置,admin這個url訪問時需要有admin或ADMIN權限
4,UserAccessDeniedHandler.java
@Component("UserAccessDeniedHandler") public class UserAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { //當用戶在沒有授權的情況下訪問受保護的REST資源時,將調用此方法發送403 Forbidden響應 System.out.println("UserAccessDeniedHandler"); //response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); ServletUtil.printRestResult(RestResult.error(ResponseCode.WEB_403)); } }
登錄用戶拒絕訪問的處理
5,UserAuthenticationEntryPoint.java
@Component public class UserAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 當用戶嘗試訪問安全的REST資源而不提供任何憑據時,將調用此方法發送401 響應 System.out.println("i am 401"); ServletUtil.printRestResult(RestResult.error(ResponseCode.WEB_401)); } }
匿名用戶拒絕訪問的處理
6,SecUser.java
public class SecUser extends User { //用戶id private int userid; //用戶昵稱 private String nickname; public SecUser(String username, String password, Collection<? extends GrantedAuthority> authorities) { super(username, password, authorities); } public SecUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public int getUserid() { return userid; } public void setUserid(int userid) { this.userid = userid; } }
擴展spring security的user類
7,SecUserDetailService.java
@Component("SecUserDetailService") public class SecUserDetailService implements UserDetailsService{ @Resource private SysUserService sysUserService; //從數據庫查詢得到用戶信息 @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //查庫 SysUser oneUser = sysUserService.getOneUserByUsername(s);//數據庫查詢 看用戶是否存在 String encodedPassword = oneUser.getPassword(); Collection<GrantedAuthority> collection = new ArrayList<>();//權限集合 //用戶權限:需要加 ROLE_ List<String> roles = oneUser.getRoles(); //System.out.println(roles); for (String roleone : roles) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+roleone); collection.add(grantedAuthority); } //增加用戶的userid,nickname SecUser user = new SecUser(s,encodedPassword,collection); user.setUserid(oneUser.getUserId()); user.setNickname(oneUser.getNickName()); return user; } }
通過查詢數據庫得到用戶信息
8,其他非關鍵代碼不一一貼出,可以從github訪問
五,測試效果
1,得到token:
用postman訪問:
http://127.0.0.1:8080/oauth/token
如圖:
發送請求后效果:
此時我們可以使用 access_token訪問資源服務器了
2,用postman訪問:
http://127.0.0.1:8080/admin/hello?access_token=GOuq97fKw-O2eo-3yPp7jrTXc4A
說明:此處的access_token是我們上面所生成的字串
返回:
this is admin method
說明訪問成功
3,刷新token,用postman訪問:
http://127.0.0.1:8080/oauth/token
如圖:
說明:此處使用的refresh_token是得到token時所返回的
4,查看redis中所保存的token?
登錄到redis查看
127.0.0.1:6379> keys * 1) "access_to_refresh:mei0DkuqTAXw7K70tfcJpg43ERY" 2) "refresh_to_access:-aCSypexyqcfJNfdaO3GGlqfSVU" 3) "uname_to_access:client_id:admin" 4) "auth:mei0DkuqTAXw7K70tfcJpg43ERY" 5) "client_id_to_access:client_id" 6) "access:mei0DkuqTAXw7K70tfcJpg43ERY" 7) "refresh_auth:-aCSypexyqcfJNfdaO3GGlqfSVU" 8) "auth_to_access:8d9934986188793067df3115293372b7" 9) "refresh:-aCSypexyqcfJNfdaO3GGlqfSVU"
5,如果換成無權限的賬號,是否還能訪問/admin/hello?
這次使用lhd這個賬號:
因為當前賬號沒有被授權,訪問時會報錯:
六,查看spring boot的版本
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.3.RELEASE)