spring boot:spring security實現oauth2+jwt管理認證授權及oauth2返回結果格式化(spring boot 2.3.3)
一,為什麼oauth2要整合jwt?
1,OAuth2的token技術有一個最大的問題是不攜帶使用者資訊,所以資源伺服器不能進行本地驗證,
以致每次對於資源的訪問,資源伺服器都需要向認證伺服器的token儲存發起請求,
一是驗證token的有效性,二是獲取token對應的使用者資訊。
有大量的請求時會導致處理效率降低,
而且認證伺服器作為一箇中心節點,
對於SLA和處理效能等均有很高的要求
對於分散式架構都是可能引發問題的隱患
2,jwt技術的兩個優勢:
token的簽名驗證可以直接在資源伺服器本地完成,不需要再次連線認證伺服器
jwt的payload部分可以儲存使用者相關資訊,這樣直接有了token和使用者資訊的繫結
3,spring security oauth2生成token時的輸出預設格式不能直接修改,
演示例子中通過增加一個controller實現了輸出的格式化
說明:劉巨集締的架構森林是一個專注架構的部落格,地址:https://www.cnblogs.com/architectforest
對應的原始碼可以訪問這裡獲取:https://github.com/liuhongdi/
說明:作者:劉巨集締 郵箱: [email protected]
二,演示專案的相關資訊
1,專案地址:
https://github.com/liuhongdi/securityoauth2jwt
2,專案功能說明:
演示了使用jwt儲存oauth2的token
3,專案結構:如圖:
三,配置檔案說明
1,pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--security begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--oauth2 begin--> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.5.0.RELEASE</version> </dependency> <!--jwt begin--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.1.1.RELEASE</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> <!--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> <!--jjwt begin--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--validation begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
2,application.properties
#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 @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) 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.csrf().disable(); 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,AuthorizationServerConfig.java
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Resource AuthenticationManager authenticationManager; @Resource UserDetailsService userDetailsService; @Resource TokenStore jwtTokenStore; @Resource JwtAccessTokenConverter jwtAccessTokenConverter; @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(jwtTokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .accessTokenConverter(jwtAccessTokenConverter); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //允許客戶端表單身份驗證 security.allowFormAuthenticationForClients(); } }
授權伺服器的配置
3,ResourceServerConfig.java
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String RESOURCE_ID = "rid"; @Resource private RestAuthenticationEntryPoint restAuthenticationEntryPoint; @Resource private RestAccessDeniedHandler restAccessDeniedHandler; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID).stateless(false) .authenticationEntryPoint(restAuthenticationEntryPoint); } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasAnyRole("admin","ADMIN"); http. anonymous().disable() .authorizeRequests() .antMatchers("/users/**").access("hasRole('ADMIN')") .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler()); //http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint); http.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler); } }
資源伺服器的配置
4,RestAccessDeniedHandler.java
@Component("restAccessDeniedHandler") public class RestAccessDeniedHandler implements AccessDeniedHandler { //處理許可權不足的情況:403,對應:access_denied @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { System.out.println("-------RestAccessDeniedHandler"); ServletUtil.printRestResult(RestResult.error(403,"許可權不夠訪問當前資源,被拒絕")); } }
遇到access deny情況的處理
5,RestAuthenticationEntryPoint.java
@Component("restAuthenticationEntryPoint") public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { //返回未得到授權時的報錯:對應:invalid_token @Override public void commence( HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { //System.out.println("commence"); ServletUtil.printRestResult(RestResult.error(401,"未得到授權")); } }
匿名使用者無權訪問時的處理
6,JwtTokenConfig.java
@Configuration public class JwtTokenConfig { @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } //使用Jwt來作為token的生成 @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("internet_plus"); return accessTokenConverter; } @Bean public TokenEnhancer jwtTokenEnhancer(){ return new JwtTokenEnhancer (); } }
配置jwttoken,指定了signkey
7,JwtTokenEnhancer.java
public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String,Object> info = new HashMap<>(); info.put("provider","haolarn"); //設定附加資訊 ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info); return null; } }
返回token資訊中的附加資訊
8,OauthController.java
@RestController @RequestMapping("/oauth") public class OauthController { @Autowired private TokenEndpoint tokenEndpoint; //自定義返回資訊新增基本資訊 @PostMapping("/token") public RestResult postAccessTokenWithUserInfo(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { OAuth2AccessToken accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); Map<String, Object> data = new LinkedHashMap(); data.put("accessToken", accessToken.getValue()); data.put("token_type", accessToken.getTokenType()); data.put("refreshToken", accessToken.getRefreshToken().getValue()); data.put("scope", accessToken.getScope()); data.put("expires_in", accessToken.getExpiresIn()); data.put("jti", accessToken.getAdditionalInformation().get("jti")); return RestResult.success(data); } }
格式化生成token時的返回json資訊
9,其他非關鍵程式碼可以訪問github檢視,不再一一貼出
五,測試效果
1,得到基於jwt的token
訪問:http://127.0.0.1:8080/oauth/token
返回結果:
2,用得到的access_token訪問admin/hello:檢視當前使用者和role:
http://127.0.0.1:8080/admin/hello
返回:
3,換一個無許可權的賬號登入:
訪問admin/hello時會提示無許可權
六,檢視spring boot的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.3.RELEASE)