《Spring Security實現OAuth2.0受權服務 - 基礎版》介紹瞭如何使用Spring Security實現OAuth2.0受權和資源保護,可是使用的都是Spring Security默認的登陸頁、受權頁,client和token信息也是保存在內存中的。javascript
本文將介紹如何在Spring Security OAuth項目中自定義登陸頁面、自定義受權頁面、數據庫配置client信息、數據庫保存受權碼和token令牌。css
須要在基礎版之上引入thymeleaf、JDBC、mybatis、mysql等依賴。html
1 <!-- thymeleaf --> 2 <dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-thymeleaf</artifactId> 5 </dependency> 6 <dependency> 7 <groupId>org.thymeleaf.extras</groupId> 8 <artifactId>thymeleaf-extras-springsecurity4</artifactId> 9 </dependency> 10 11 <!-- JDBC --> 12 <dependency> 13 <groupId>org.springframework.boot</groupId> 14 <artifactId>spring-boot-starter-jdbc</artifactId> 15 </dependency> 16 <dependency> 17 <groupId>org.apache.commons</groupId> 18 <artifactId>commons-dbcp2</artifactId> 19 </dependency> 20 21 <!-- Mybatis --> 22 <dependency> 23 <groupId>org.mybatis.spring.boot</groupId> 24 <artifactId>mybatis-spring-boot-starter</artifactId> 25 <version>1.1.1</version> 26 </dependency> 27 28 <!-- MySQL --> 29 <dependency> 30 <groupId>mysql</groupId> 31 <artifactId>mysql-connector-java</artifactId> 32 </dependency>
1 -- used in tests that use HSQL 2 create table oauth_client_details ( 3 client_id VARCHAR(255) PRIMARY KEY, 4 resource_ids VARCHAR(255), 5 client_secret VARCHAR(255), 6 scope VARCHAR(255), 7 authorized_grant_types VARCHAR(255), 8 web_server_redirect_uri VARCHAR(255), 9 authorities VARCHAR(255), 10 access_token_validity INTEGER, 11 refresh_token_validity INTEGER, 12 additional_information TEXT(4096), 13 autoapprove VARCHAR(255) 14 ); 15 16 create table oauth_client_token ( 17 token_id VARCHAR(255), 18 token BLOB, 19 authentication_id VARCHAR(255) PRIMARY KEY, 20 user_name VARCHAR(255), 21 client_id VARCHAR(255) 22 ); 23 24 create table oauth_access_token ( 25 token_id VARCHAR(255), 26 token BLOB, 27 authentication_id VARCHAR(255) PRIMARY KEY, 28 user_name VARCHAR(255), 29 client_id VARCHAR(255), 30 authentication BLOB, 31 refresh_token VARCHAR(255) 32 ); 33 34 create table oauth_refresh_token ( 35 token_id VARCHAR(255), 36 token BLOB, 37 authentication BLOB 38 ); 39 40 create table oauth_code ( 41 code VARCHAR(255), authentication BLOB 42 ); 43 44 create table oauth_approvals ( 45 userId VARCHAR(255), 46 clientId VARCHAR(255), 47 scope VARCHAR(255), 48 status VARCHAR(10), 49 expiresAt TIMESTAMP, 50 lastModifiedAt TIMESTAMP 51 ); 52 53 54 -- customized oauth_client_details table 55 create table ClientDetails ( 56 appId VARCHAR(255) PRIMARY KEY, 57 resourceIds VARCHAR(255), 58 appSecret VARCHAR(255), 59 scope VARCHAR(255), 60 grantTypes VARCHAR(255), 61 redirectUrl VARCHAR(255), 62 authorities VARCHAR(255), 63 access_token_validity INTEGER, 64 refresh_token_validity INTEGER, 65 additionalInformation VARCHAR(4096), 66 autoApproveScopes VARCHAR(255) 67 );
在oauth_client_details表添加數據:java
1 INSERT INTO `oauth_client_details` VALUES ('net5ijy', NULL, '123456', 'all,read,write', 'authorization_code,refresh_token,password', NULL, 'ROLE_TRUSTED_CLIENT', 7200, 7200, NULL, NULL); 2 INSERT INTO `oauth_client_details` VALUES ('tencent', NULL, '123456', 'all,read,write', 'authorization_code,refresh_code', NULL, 'ROLE_TRUSTED_CLIENT', 3600, 3600, NULL, NULL);
1 CREATE TABLE `springcloud_user` ( 2 `id` int(11) NOT NULL AUTO_INCREMENT , 3 `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , 4 `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , 5 `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , 6 `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , 7 `create_time` datetime NOT NULL , 8 PRIMARY KEY (`id`) 9 ) 10 ENGINE=InnoDB 11 DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci 12 AUTO_INCREMENT=1; 13 14 CREATE TABLE `springcloud_role` ( 15 `id` int(11) NOT NULL AUTO_INCREMENT , 16 `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , 17 PRIMARY KEY (`id`) 18 ) 19 ENGINE=InnoDB 20 DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci 21 AUTO_INCREMENT=1; 22 23 CREATE TABLE `springcloud_user_role` ( 24 `user_id` int(11) NOT NULL , 25 `role_id` int(11) NOT NULL , 26 FOREIGN KEY (`role_id`) REFERENCES `springcloud_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, 27 FOREIGN KEY (`user_id`) REFERENCES `springcloud_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, 28 INDEX `user_id_fk` USING BTREE (`user_id`) , 29 INDEX `role_id_fk` USING BTREE (`role_id`) 30 ) 31 ENGINE=InnoDB 32 DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci;
用戶表添加數據mysql
1 INSERT INTO `springcloud_user` VALUES (1, 'admin001', '$2a$10$sXHKvdufrEfE2900ME40nOSBmeHRRUOF71szu22uaqqL8FIJeJDYW', '13622114309', '13622114309@189.cn', '2019-4-7 09:31:07'); 2 INSERT INTO `springcloud_user` VALUES (2, 'admin002', '$2a$10$sXHKvdufrEfE2900ME40nOSBmeHRRUOF71szu22uaqqL8FIJeJDYW', '17809837654', '17809837654@189.cn', '2019-4-7 09:33:00');
角色表添加數據web
1 INSERT INTO `springcloud_role` VALUES (1, 'ADMIN'); 2 INSERT INTO `springcloud_role` VALUES (2, 'DBA'); 3 INSERT INTO `springcloud_role` VALUES (3, 'USER');
用戶角色關係表添加數據spring
1 INSERT INTO `springcloud_user_role` VALUES (1, 1); 2 INSERT INTO `springcloud_user_role` VALUES (2, 1);
封裝受權服務器登陸用戶信息sql
1 public class User implements Serializable { 2 3 private Integer id; 4 private String username; 5 private String password; 6 private String phone; 7 private String email; 8 private Set<Role> roles = new HashSet<Role>(); 9 private Date createTime; 10 11 // getter & setter 12 13 @Override 14 public int hashCode() { 15 final int prime = 31; 16 int result = 1; 17 result = prime * result + ((id == null) ? 0 : id.hashCode()); 18 return result; 19 } 20 @Override 21 public boolean equals(Object obj) { 22 if (this == obj) 23 return true; 24 if (obj == null) 25 return false; 26 if (getClass() != obj.getClass()) 27 return false; 28 User other = (User) obj; 29 if (id == null) { 30 if (other.id != null) 31 return false; 32 } else if (!id.equals(other.id)) 33 return false; 34 return true; 35 } 36 @Override 37 public String toString() { 38 return "User [id=" + id + ", username=" + username + ", password=" 39 + password + ", phone=" + phone + ", email=" + email 40 + ", roles=" + roles + ", createTime=" + createTime + "]"; 41 } 42 }
封裝角色信息數據庫
1 public class Role implements Serializable { 2 3 private Integer id; 4 private String name; 5 6 public Role() { 7 super(); 8 } 9 public Role(String name) { 10 super(); 11 this.name = name; 12 } 13 14 // getter & setter 15 16 @Override 17 public String toString() { 18 return "Role [id=" + id + ", name=" + name + "]"; 19 } 20 }
封裝接口響應信息apache
1 public class ResponseMessage { 2 3 private Integer code; 4 private String message; 5 6 public ResponseMessage() { 7 super(); 8 } 9 10 public ResponseMessage(Integer code, String message) { 11 super(); 12 this.code = code; 13 this.message = message; 14 } 15 16 // getter & setter 17 18 public static ResponseMessage success() { 19 return new ResponseMessage(0, "操做成功"); 20 } 21 22 public static ResponseMessage fail() { 23 return new ResponseMessage(99, "操做失敗"); 24 } 25 }
在application.properties文件配置datasource
1 spring.datasource.url=jdbc:mysql://localhost:3306/test 2 spring.datasource.username=system 3 spring.datasource.password=123456 4 spring.datasource.driver-class-name=com.mysql.jdbc.Driver 5 6 spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource 7 spring.datasource.dbcp2.initial-size=5 8 spring.datasource.dbcp2.max-active=25 9 spring.datasource.dbcp2.max-idle=10 10 spring.datasource.dbcp2.min-idle=5 11 spring.datasource.dbcp2.max-wait-millis=10000 12 spring.datasource.dbcp2.validation-query=SELECT 1 13 spring.datasource.dbcp2.connection-properties=characterEncoding=utf8
使用dbcp2數據源
在src/main/resources下建立org.net5ijy.oauth2.mapper包,建立user-mapper.xml配置文件
1 <mapper namespace="org.net5ijy.oauth2.repository.UserRepository"> 2 3 <resultMap type="User" id="UserResultMap"> 4 <result column="id" property="id" jdbcType="INTEGER" javaType="int" /> 5 <result column="username" property="username" jdbcType="VARCHAR" 6 javaType="string" /> 7 <result column="password" property="password" jdbcType="VARCHAR" 8 javaType="string" /> 9 <result column="phone" property="phone" jdbcType="VARCHAR" 10 javaType="string" /> 11 <result column="email" property="email" jdbcType="VARCHAR" 12 javaType="string" /> 13 <result column="create_time" property="createTime" jdbcType="TIMESTAMP" 14 javaType="java.util.Date" /> 15 <collection property="roles" select="selectRolesByUserId" 16 column="id"></collection> 17 </resultMap> 18 19 <!-- 根據用戶名查詢用戶 --> 20 <select id="findByUsername" parameterType="java.lang.String" 21 resultMap="UserResultMap"> 22 <![CDATA[ 23 select * from springcloud_user where username = #{username} 24 ]]> 25 </select> 26 27 <!-- 根據user id查詢用戶擁有的role --> 28 <select id="selectRolesByUserId" parameterType="java.lang.Integer" 29 resultType="Role"> 30 <![CDATA[ 31 select r.id, r.name from springcloud_user_role ur, springcloud_role r 32 where ur.role_id = r.id and ur.user_id = #{id} 33 ]]> 34 </select> 35 36 </mapper>
由於咱們的例子只使用了findByUsername功能,因此只寫這個sql就能夠了
1 public interface UserRepository { 2 3 User findByUsername(String username); 4 }
接口
1 public interface UserService { 2 3 User getUser(String username); 4 }
實現類
1 @Service 2 public class UserServiceImpl implements UserService { 3 4 static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); 5 6 @Autowired 7 private UserRepository userRepository; 8 9 @Autowired 10 private JdbcTemplate jdbcTemplate; 11 12 @Override 13 public User getUser(String username) { 14 return userRepository.findByUsername(username); 15 } 16 }
這個接口的實現類須要在Security中配置,Security會使用這個類根據用戶名查詢用戶信息,而後進行用戶名、密碼的驗證。主要就是實現loadUserByUsername方法:
1 @Service 2 public class UserDetailsServiceImpl implements UserDetailsService { 3 4 @Autowired 5 private UserService userService; 6 7 @Override 8 public UserDetails loadUserByUsername(String username) 9 throws UsernameNotFoundException { 10 11 User user = userService.getUser(username); 12 if (user == null || user.getId() < 1) { 13 throw new UsernameNotFoundException("Username not found: " 14 + username); 15 } 16 17 return new org.springframework.security.core.userdetails.User( 18 user.getUsername(), user.getPassword(), true, true, true, true, 19 getGrantedAuthorities(user)); 20 } 21 22 private Collection<? extends GrantedAuthority> getGrantedAuthorities( 23 User user) { 24 Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(); 25 for (Role role : user.getRoles()) { 26 authorities 27 .add(new SimpleGrantedAuthority("ROLE_" + role.getName())); 28 } 29 return authorities; 30 } 31 }
編寫LoginController類,添加login方法
1 @RestController 2 public class LoginController { 3 4 @GetMapping("/login") 5 public ModelAndView login() { 6 return new ModelAndView("login"); 7 } 8 9 @GetMapping("/login-error") 10 public ModelAndView loginError(HttpServletRequest request, Model model) { 11 model.addAttribute("loginError", true); 12 model.addAttribute("errorMsg", "登錄失敗,帳號或者密碼錯誤!"); 13 return new ModelAndView("login", "userModel", model); 14 } 15 }
頁面代碼使用到了thymeleaf、bootstrap、表單驗證等,具體的js、css引入就不贅述了,只記錄最主要的內容:
1 <div> 2 <form th:action="@{/login}" method="post"> 3 <div> 4 <label>用 戶 名: </label> 5 <div> 6 <input name="username" /> 7 </div> 8 </div> 9 <div> 10 <label>密  碼: </label> 11 <div> 12 <input type="password" name="password" /> 13 </div> 14 </div> 15 <div> 16 <div> 17 <button type="submit"> 登 陸 </button> 18 </div> 19 </div> 20 </form> 21 </div>
編寫GrantController類,添加getAccessConfirmation方法
1 @Controller 2 @SessionAttributes("authorizationRequest") 3 public class GrantController { 4 5 @RequestMapping("/oauth/confirm_access") 6 public ModelAndView getAccessConfirmation(Map<String, Object> model, 7 HttpServletRequest request) throws Exception { 8 9 AuthorizationRequest authorizationRequest = (AuthorizationRequest) model 10 .get("authorizationRequest"); 11 12 ModelAndView view = new ModelAndView("base-grant"); 13 view.addObject("clientId", authorizationRequest.getClientId()); 14 15 return view; 16 } 17 }
此處獲取到申請受權的clientid用於在頁面展現
此處只寫最主要的部分
1 <div> 2 <div> 3 <div>OAUTH-BOOT 受權</div> 4 <div> 5 <a href="javascript:;">幫助</a> 6 </div> 7 </div> 8 <h3 th:text="${clientId}+' 請求受權,該應用將獲取您的如下信息'"></h3> 9 <p>暱稱,頭像和性別</p> 10 受權後代表您已贊成 <a href="javascript:;" style="color: #E9686B">OAUTH-BOOT 服務協議</a> 11 <form method="post" action="/oauth/authorize"> 12 <input type="hidden" name="user_oauth_approval" value="true" /> 13 <input type="hidden" name="scope.all" value="true" /> 14 <br /> 15 <button class="btn" type="submit">贊成/受權</button> 16 </form> 17 </div>
配置SqlSessionFactoryBean
1 @Configuration 2 public class MyBatisConfiguration { 3 4 @Bean 5 @Autowired 6 @ConditionalOnMissingBean 7 public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) 8 throws IOException { 9 10 SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); 11 12 // 設置數據源 13 sqlSessionFactoryBean.setDataSource(dataSource); 14 15 // 設置別名包 16 sqlSessionFactoryBean.setTypeAliasesPackage("org.net5ijy.oauth2.bean"); 17 18 // 設置mapper映射文件所在的包 19 PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver(); 20 String packageSearchPath = "classpath*:org/net5ijy/oauth2/mapper/**.xml"; 21 sqlSessionFactoryBean 22 .setMapperLocations(pathMatchingResourcePatternResolver 23 .getResources(packageSearchPath)); 24 25 return sqlSessionFactoryBean; 26 } 27 }
1 @Configuration 2 public class Oauth2AuthorizationServerConfiguration extends 3 AuthorizationServerConfigurerAdapter { 4 5 @Autowired 6 private UserDetailsService userDetailsService; 7 8 @Autowired 9 private AuthenticationManager authenticationManager; 10 11 @Autowired 12 private DataSource dataSource; 13 14 @Override 15 public void configure(ClientDetailsServiceConfigurer clients) 16 throws Exception { 17 18 // 數據庫管理client 19 clients.withClientDetails(new JdbcClientDetailsService(dataSource)); 20 } 21 22 @Override 23 public void configure(AuthorizationServerEndpointsConfigurer endpoints) 24 throws Exception { 25 26 // 用戶信息查詢服務 27 endpoints.userDetailsService(userDetailsService); 28 29 // 數據庫管理access_token和refresh_token 30 TokenStore tokenStore = new JdbcTokenStore(dataSource); 31 32 endpoints.tokenStore(tokenStore); 33 34 ClientDetailsService clientService = new JdbcClientDetailsService( 35 dataSource); 36 37 DefaultTokenServices tokenServices = new DefaultTokenServices(); 38 tokenServices.setTokenStore(tokenStore); 39 tokenServices.setSupportRefreshToken(true); 40 tokenServices.setClientDetailsService(clientService); 41 // tokenServices.setAccessTokenValiditySeconds(180); 42 // tokenServices.setRefreshTokenValiditySeconds(180); 43 44 endpoints.tokenServices(tokenServices); 45 46 endpoints.authenticationManager(authenticationManager); 47 48 // 數據庫管理受權碼 49 endpoints.authorizationCodeServices(new JdbcAuthorizationCodeServices( 50 dataSource)); 51 // 數據庫管理受權信息 52 ApprovalStore approvalStore = new JdbcApprovalStore(dataSource); 53 endpoints.approvalStore(approvalStore); 54 } 55 }
1 @EnableWebSecurity 2 public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 3 4 @Autowired 5 private UserDetailsService userDetailsService; 6 7 @Autowired 8 private PasswordEncoder passwordEncoder; 9 10 @Bean 11 public PasswordEncoder passwordEncoder() { 12 return new BCryptPasswordEncoder(); // 使用 BCrypt 加密 13 } 14 15 public AuthenticationProvider authenticationProvider() { 16 DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); 17 authenticationProvider.setUserDetailsService(userDetailsService); 18 authenticationProvider.setPasswordEncoder(passwordEncoder); 19 authenticationProvider.setHideUserNotFoundExceptions(false); 20 return authenticationProvider; 21 } 22 23 @Override 24 public void configure(WebSecurity web) throws Exception { 25 web.ignoring().antMatchers("/css/**", "/js/**", "/fonts/**", 26 "/icon/**", "/favicon.ico"); 27 } 28 29 @Override 30 protected void configure(HttpSecurity http) throws Exception { 31 32 http.requestMatchers() 33 .antMatchers("/login", "/login-error", "/oauth/authorize", 34 "/oauth/token").and().authorizeRequests() 35 .antMatchers("/login").permitAll().anyRequest().authenticated(); 36 37 // 登陸頁面 38 http.formLogin().loginPage("/login").failureUrl("/login-error"); 39 40 // 禁用CSRF 41 http.csrf().disable(); 42 } 43 44 @Autowired 45 public void configureGlobal(AuthenticationManagerBuilder auth) 46 throws Exception { 47 auth.userDetailsService(userDetailsService); 48 auth.authenticationProvider(authenticationProvider()); 49 } 50 51 public static void main(String[] args) { 52 System.out.println(new BCryptPasswordEncoder().encode("123456")); 53 } 54 }
1 server.port=7001 2 3 ##### Built-in DataSource ##### 4 spring.datasource.url=jdbc:mysql://localhost:3306/test 5 spring.datasource.username=system 6 spring.datasource.password=123456 7 spring.datasource.driver-class-name=com.mysql.jdbc.Driver 8 9 spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource 10 spring.datasource.dbcp2.initial-size=5 11 spring.datasource.dbcp2.max-active=25 12 spring.datasource.dbcp2.max-idle=10 13 spring.datasource.dbcp2.min-idle=5 14 spring.datasource.dbcp2.max-wait-millis=10000 15 spring.datasource.dbcp2.validation-query=SELECT 1 16 spring.datasource.dbcp2.connection-properties=characterEncoding=utf8 17 18 ##### Thymeleaf ##### 19 # 編碼 20 spring.thymeleaf.encoding=UTF-8 21 # 熱部署靜態文件 22 spring.thymeleaf.cache=false 23 # 使用HTML5標準 24 spring.thymeleaf.mode=HTML5
1 @RestController 2 @RequestMapping(value = "/order") 3 public class TestController { 4 5 Logger log = LoggerFactory.getLogger(TestController.class); 6 7 @RequestMapping(value = "/demo") 8 @ResponseBody 9 public ResponseMessage getDemo() { 10 Authentication auth = SecurityContextHolder.getContext() 11 .getAuthentication(); 12 log.info(auth.toString()); 13 return ResponseMessage.success(); 14 } 15 }
1 @SpringBootApplication 2 @EnableAuthorizationServer 3 @EnableResourceServer 4 @MapperScan("org.net5ijy.oauth2.repository") 5 public class Oauth2Application { 6 7 public static void main(String[] args) { 8 9 // args = new String[] { "--debug" }; 10 11 SpringApplication.run(Oauth2Application.class, args); 12 } 13 }
使用瀏覽器訪問:
http://localhost:7001/oauth/authorize?response_type=code&client_id=net5ijy&redirect_uri=http://localhost:8080&scope=all
地址
http://localhost:7001/oauth/authorize
參數
response_type |
code |
client_id |
根據實際的client-id填寫,此處寫net5ijy |
redirect_uri |
生成code後的回調地址,http://localhost:8080 |
scope |
權限範圍 |
登陸,admin001和123456
容許受權
看到瀏覽器重定向到了http://localhost:8080並攜帶了code參數,這個code就是受權服務器生成的受權碼
使用curl命令獲取token令牌
curl --user net5ijy:123456 -X POST -d "grant_type=authorization_code&scope=all&redirect_uri=http%3a%2f%2flocalhost%3a8080&code=ubtvR4" http://localhost:7001/oauth/token
地址
http://localhost:7001/oauth/token
參數
grant_type |
受權碼模式,寫authorization_code |
scope |
權限範圍 |
redirect_uri |
回調地址,http://localhost:8080須要urlencode |
code |
就是上一步生成的受權碼 |
返回值
1 { 2 "access_token": "c5836918-1924-4b0a-be67-043218c6e7e0", 3 "token_type": "bearer", 4 "refresh_token": "7950b7f9-7d60-41da-9a95-bd2c8b29ada1", 5 "expires_in": 7199, 6 "scope": "all" 7 }
這樣就獲取到了token令牌,該token的訪問權限範圍是all權限,在2小時後失效。
http://localhost:7001/order/demo?access_token=c5836918-1924-4b0a-be67-043218c6e7e0