Spring?Clou整合?Security?+?Oauth2?+?jwt實現(xiàn)權(quán)限認證的詳細過程
前言
OAuth2是一個開放的標準,協(xié)議。即允許用戶讓第三方應用訪問某一個網(wǎng)站上存儲的用戶私密資源(照片,頭像等)。這個過程中無需將用戶名和密碼提供給第三方應用。在互聯(lián)網(wǎng)中,我們最常見的OAuth2的應用就是各種第三方通過QQ授權(quán),微信授權(quán),微博授權(quán)等登錄了。
OAuth2協(xié)議一共支持4中不同的授權(quán)模式。
- 授權(quán)碼模式:常見的第三方平臺登錄功能基本上都使用這種模式。
- 簡化模式:簡化模式不想要客戶端服務器(第三方應用服務器)參與,直接在瀏覽器中向授權(quán)服務器申請令牌。
- 密碼模式:密碼模式是用戶把用戶名,密碼直接告訴客戶端,客戶端使用這些信息向授權(quán)服務器申請令牌。這需要用戶對客戶端高度信任。
- 客戶端模式:客戶端模式是指客戶端使用自己的名義而不是用戶的名義向服務器申請授權(quán)。
OAuth2的重要參數(shù)
①response_type
code:表示要求返回授權(quán)碼。token:表示直接返回令牌
②client_id
客戶端身份標識
③client_secret
客戶端密鑰
④redirect_uri
重定向地址
⑤scope
表示授權(quán)的范圍。read:只讀權(quán)限,all讀寫權(quán)限
⑥grant_type
表示授權(quán)的方式。AUTHORIZATION_CODE(授權(quán)碼),PASSWORD(密碼),CLIENT_CREDENTIALS(品正式),REFRESH_TOKEN(更新令牌)
⑦state
應用程序傳遞的一個隨機數(shù),用來防止CSRF攻擊
話不多說直接上代碼
一、創(chuàng)建項目
首先創(chuàng)建一個主體:
SpringSecurityOauth2jtw
子項目:
admin——登錄
oauth2——權(quán)限
common——公共
gateway——網(wǎng)關(guān)
二、步驟
1.引入依賴
代碼如下(示例):
主體依賴:
<java.version>1.8</java.version> <spring-boot.version>2.7.0</spring-boot.version> <spring-cloud.version>2021.0.3</spring-cloud.version> <spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version> <spring-cloud-starter-oauth2.version>2.2.5.RELEASE</spring-cloud-starter-oauth2.version> <!--Mysql數(shù)據(jù)庫驅(qū)動--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector.version}</version> </dependency> <!--SpringData工具包--> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> <version>${spring-data-commons.version}</version> </dependency> <!--JWT(Json Web Token)登錄支持--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jjwt.version}</version> </dependency> <!--JWT(Json Web Token)登錄支持--> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>${nimbus-jose-jwt.version}</version> </dependency> <!--集成druid連接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <!--Hutool Java工具包--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> <!--Spring Cloud 相關(guān)依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--Spring Cloud Alibaba 相關(guān)依賴--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency>
common依賴導入:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> </dependency> <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
oauth2依賴導入:
<dependency> <groupId>com.common</groupId> <artifactId>common</artifactId> <version>1.0-SNAPSHOT</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>${spring-cloud-starter-oauth2.version}</version> </dependency> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
admin依賴導入:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.common</groupId> <artifactId>common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
2.gateway代碼部分
代碼如下(示例):
配置鑒權(quán)管理器
@Component public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private IgnoreUrlsConfig ignoreUrlsConfig; @Override public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) { ServerHttpRequest request = authorizationContext.getExchange().getRequest(); URI uri = request.getURI(); PathMatcher pathMatcher = new AntPathMatcher(); //白名單路徑直接放行 List<String> ignoreUrls = ignoreUrlsConfig.getUrls(); for (String ignoreUrl : ignoreUrls) { if (pathMatcher.match(ignoreUrl, uri.getPath())) { return Mono.just(new AuthorizationDecision(true)); } } //對應跨域的預檢請求直接放行 if(request.getMethod()==HttpMethod.OPTIONS){ return Mono.just(new AuthorizationDecision(true)); } //不同用戶體系登錄不允許互相訪問 try { String token = request.getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER); if(StrUtil.isEmpty(token)){ return Mono.just(new AuthorizationDecision(false)); } String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, ""); JWSObject jwsObject = JWSObject.parse(realToken); String userStr = jwsObject.getPayload().toString(); UserDto userDto = JSONUtil.toBean(userStr, UserDto.class); if (AuthConstant.ADMIN_CLIENT_ID.equals(userDto.getClientId()) && !pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) { return Mono.just(new AuthorizationDecision(false)); } if (AuthConstant.PORTAL_CLIENT_ID.equals(userDto.getClientId()) && pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) { return Mono.just(new AuthorizationDecision(false)); } } catch (ParseException e) { e.printStackTrace(); return Mono.just(new AuthorizationDecision(false)); } //非管理端路徑直接放行 if (!pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())) { return Mono.just(new AuthorizationDecision(true)); } //管理端路徑需校驗權(quán)限 Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstant.RESOURCE_ROLES_MAP_KEY); Iterator<Object> iterator = resourceRolesMap.keySet().iterator(); List<String> authorities = new ArrayList<>(); while (iterator.hasNext()) { String pattern = (String) iterator.next(); if (pathMatcher.match(pattern, uri.getPath())) { authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern))); } } authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList()); //認證通過且角色匹配的用戶可訪問當前路徑 return mono .filter(Authentication::isAuthenticated) .flatMapIterable(Authentication::getAuthorities) .map(GrantedAuthority::getAuthority) .any(authorities::contains) .map(AuthorizationDecision::new) .defaultIfEmpty(new AuthorizationDecision(false)); }
未登錄或者token失效時的返回結(jié)果:
@Component public class RestAuthenticationEntryPoint implements ServerAuthenticationEntryPoint { @Override public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.OK); response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); response.getHeaders().set("Access-Control-Allow-Origin","*"); response.getHeaders().set("Cache-Control","no-cache"); String body= JSONUtil.toJsonStr(CommonResult.unauthorized(e.getMessage())); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8"))); return response.writeWith(Mono.just(buffer)); } }
無權(quán)限訪問的返回結(jié)果:
@Component public class RestfulAccessDeniedHandler implements ServerAccessDeniedHandler { @Override public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.OK); response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); response.getHeaders().set("Access-Control-Allow-Origin","*"); response.getHeaders().set("Cache-Control","no-cache"); String body= JSONUtil.toJsonStr(CommonResult.forbidden(denied.getMessage())); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8"))); return response.writeWith(Mono.just(buffer)); } }
全局跨域配置:注意:前端從網(wǎng)關(guān)進行調(diào)用時需要配置
@Configuration public class GlobalCorsConfig { @Bean public CorsWebFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedMethod("*"); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }
網(wǎng)關(guān)白名單配置:
@Data @EqualsAndHashCode(callSuper = false) @Component @ConfigurationProperties(prefix="secure.ignore") public class IgnoreUrlsConfig { private List<String> urls; }
資源服務器配置:
@AllArgsConstructor @Configuration @EnableWebFluxSecurity public class ResourceServerConfig { private final AuthorizationManager authorizationManager; private final IgnoreUrlsConfig ignoreUrlsConfig; private final RestfulAccessDeniedHandler restfulAccessDeniedHandler; private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter; @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.oauth2ResourceServer().jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()); //自定義處理JWT請求頭過期或簽名錯誤的結(jié)果 http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint); //對白名單路徑,直接移除JWT請求頭 http.addFilterBefore(ignoreUrlsRemoveJwtFilter,SecurityWebFiltersOrder.AUTHENTICATION); http.authorizeExchange() //白名單配置 .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll() //鑒權(quán)管理器配置 .anyExchange().access(authorizationManager) .and().exceptionHandling() //處理未授權(quán) .accessDeniedHandler(restfulAccessDeniedHandler) //處理未認證 .authenticationEntryPoint(restAuthenticationEntryPoint) .and().csrf().disable(); return http.build(); } @Bean public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); } }
Swagger資源配置:
@Slf4j @Component @Primary @AllArgsConstructor public class SwaggerResourceConfig implements SwaggerResourcesProvider { private final RouteLocator routeLocator; private final GatewayProperties gatewayProperties; @Override public List<SwaggerResource> get() { List<SwaggerResource> resources = new ArrayList<>(); List<String> routes = new ArrayList<>(); //獲取所有路由的ID routeLocator.getRoutes().subscribe(route -> routes.add(route.getId())); //過濾出配置文件中定義的路由->過濾出Path Route Predicate->根據(jù)路徑拼接成api-docs路徑->生成SwaggerResource gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> { route.getPredicates().stream() .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName())) .forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(), predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0") .replace("**", "v2/api-docs")))); }); return resources; } private SwaggerResource swaggerResource(String name, String location) { log.info("name:{},location:{}", name, location); SwaggerResource swaggerResource = new SwaggerResource(); swaggerResource.setName(name); swaggerResource.setLocation(location); swaggerResource.setSwaggerVersion("2.0"); return swaggerResource; } }
自定義Swagger的各個配置節(jié)點:
@RestController public class SwaggerHandler { @Autowired(required = false) private SecurityConfiguration securityConfiguration; @Autowired(required = false) private UiConfiguration uiConfiguration; private final SwaggerResourcesProvider swaggerResources; @Autowired public SwaggerHandler(SwaggerResourcesProvider swaggerResources) { this.swaggerResources = swaggerResources; } /** * Swagger安全配置,支持oauth和apiKey設(shè)置 */ @GetMapping("/swagger-resources/configuration/security") public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK)); } /** * Swagger UI配置 */ @GetMapping("/swagger-resources/configuration/ui") public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK)); } /** * Swagger資源配置,微服務中這各個服務的api-docs信息 */ @GetMapping("/swagger-resources") public Mono<ResponseEntity> swaggerResources() { return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK))); } }
將登錄用戶的JWT轉(zhuǎn)化成用戶信息的全局過濾器:
@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER); if (StrUtil.isEmpty(token)) { return chain.filter(exchange); } try { //從token中解析用戶信息并設(shè)置到Header中去 String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, ""); JWSObject jwsObject = JWSObject.parse(realToken); String userStr = jwsObject.getPayload().toString(); LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr); ServerHttpRequest request = exchange.getRequest().mutate().header(AuthConstant.USER_TOKEN_HEADER, userStr).build(); exchange = exchange.mutate().request(request).build(); } catch (ParseException e) { e.printStackTrace(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
白名單路徑訪問時移除JWT請求頭的過濾器:
@Component public class IgnoreUrlsRemoveJwtFilter implements WebFilter { @Autowired private IgnoreUrlsConfig ignoreUrlsConfig; @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); URI uri = request.getURI(); PathMatcher pathMatcher = new AntPathMatcher(); //白名單路徑移除JWT請求頭 List<String> ignoreUrls = ignoreUrlsConfig.getUrls(); for (String ignoreUrl : ignoreUrls) { if (pathMatcher.match(ignoreUrl, uri.getPath())) { request = exchange.getRequest().mutate().header(AuthConstant.JWT_TOKEN_HEADER, "").build(); exchange = exchange.mutate().request(request).build(); return chain.filter(exchange); } } return chain.filter(exchange); } }
yml配置:
spring: mvc: pathmatch: matching-strategy: ant_path_matcher cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true #使用小寫service-id routes: #配置路由路徑 - id: oauth uri: lb://oauth predicates: - Path=/oauth/** filters: - StripPrefix=1 - id: admin uri: lb://admin #這里是配置的nacos predicates: - Path=/admin/** filters: - StripPrefix=1 security: oauth2: resourceserver: jwt: jwk-set-uri: 'http://localhost:8201/oauth/rsa/publicKey' #配置RSA的公鑰訪問地址 redis: host: localhost # Redis服務器地址 database: 0 # Redis數(shù)據(jù)庫索引(默認為0) port: 6379 # Redis服務器連接端口 password: # Redis服務器連接密碼(默認為空) timeout: 3000ms # 連接超時時間(毫秒) secure: ignore: urls: #配置白名單路徑 - "/doc.html" - "/swagger-resources/**" - "/swagger/**" - "/*/v2/api-docs" - "/*/*.js" - "/*/*.css" - "/*/*.png" - "/*/*.ico" - "/webjars/**" - "/actuator/**" - "/oauth/oauth/token" - "/oauth/rsa/publicKey" - "/admin/admin/login" - "/admin/admin/register" management: #開啟SpringBoot Admin的監(jiān)控 endpoints: web: exposure: include: '*' endpoint: health: show-details: always logging: level: root: info com.macro.mall: debug logstash: host: localhost #spring: # application: # name: gateway # cloud: # nacos: # config: # server-addr: 127.0.0.1:8848 # username: nacos # password: nacos # namespace: 4e903430-c64b-4c68-a43c-59478dd173e6 # group: DEFAULT_GROUP # prefix: ${spring.application.name} # file-extension: yaml
3.oauth2代碼部分
JWT內(nèi)容增強器:
@Component public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); Map<String, Object> info = new HashMap<>(); //把用戶ID設(shè)置到JWT中 info.put("id", securityUser.getId()); info.put("client_id",securityUser.getClientId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }
認證服務器配置:
@AllArgsConstructor @Configuration @EnableAuthorizationServer public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter { private final PasswordEncoder passwordEncoder; private final UserServiceImpl userDetailsService; private final AuthenticationManager authenticationManager; private final JwtTokenEnhancer jwtTokenEnhancer; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("admin-app") .secret(passwordEncoder.encode("123456")) .scopes("all") .authorizedGrantTypes("password", "refresh_token") .accessTokenValiditySeconds(3600*24) .refreshTokenValiditySeconds(3600*24*7) .and() .withClient("portal-app") .secret(passwordEncoder.encode("123456")) .scopes("all") .authorizedGrantTypes("password", "refresh_token") .accessTokenValiditySeconds(3600*24) .refreshTokenValiditySeconds(3600*24*7); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> delegates = new ArrayList<>(); delegates.add(jwtTokenEnhancer); delegates.add(accessTokenConverter()); enhancerChain.setTokenEnhancers(delegates); //配置JWT的內(nèi)容增強器 endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService) //配置加載用戶信息的服務 .accessTokenConverter(accessTokenConverter()) .tokenEnhancer(enhancerChain); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyPair()); return jwtAccessTokenConverter; } @Bean public KeyPair keyPair() { //從classpath下的證書中獲取秘鑰對 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); } }
Swagger相關(guān)配置:
@Configuration @EnableSwagger2 public class SwaggerConfig extends BaseSwaggerConfig { @Override public SwaggerProperties swaggerProperties() { return SwaggerProperties.builder() .apiBasePackage("com.oauth.oauth2.controller") .title("認證中心") .description("認證中心相關(guān)接口文檔") .contactName("oauth") .version("1.0") .enableSecurity(true) .build(); } @Bean public BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { return generateBeanPostProcessor(); } }
SpringSecurity相關(guān)配置:
@Configuration @EnableWebSecurity public class WebSecurityConfig{ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
消息常量定義:
public class MessageConstant { public static final String LOGIN_SUCCESS = "登錄成功!"; public static final String USERNAME_PASSWORD_ERROR = "用戶名或密碼錯誤!"; public static final String CREDENTIALS_EXPIRED = "該賬戶的登錄憑證已過期,請重新登錄!"; public static final String ACCOUNT_DISABLED = "該賬戶已被禁用,請聯(lián)系管理員!"; public static final String ACCOUNT_LOCKED = "該賬號已被鎖定,請聯(lián)系管理員!"; public static final String ACCOUNT_EXPIRED = "該賬號已過期,請聯(lián)系管理員!"; public static final String PERMISSION_DENIED = "沒有訪問權(quán)限,請聯(lián)系管理員!"; }
自定義Oauth2獲取令牌接口:
@RestController @Api(tags = "AuthController", description = "認證中心登錄認證") @RequestMapping("/oauth") public class AuthController { @Autowired private TokenEndpoint tokenEndpoint; @ApiOperation("Oauth2獲取token") @RequestMapping(value = "/token", method = RequestMethod.POST) public CommonResult<Oauth2TokenDto> postAccessToken(HttpServletRequest request, @ApiParam("授權(quán)模式") @RequestParam String grant_type, @ApiParam("Oauth2客戶端ID") @RequestParam String client_id, @ApiParam("Oauth2客戶端秘鑰") @RequestParam String client_secret, @ApiParam("刷新token") @RequestParam(required = false) String refresh_token, @ApiParam("登錄用戶名") @RequestParam(required = false) String username, @ApiParam("登錄密碼") @RequestParam(required = false) String password) throws HttpRequestMethodNotSupportedException { Map<String, String> parameters = new HashMap<>(); parameters.put("grant_type",grant_type); parameters.put("client_id",client_id); parameters.put("client_secret",client_secret); parameters.putIfAbsent("refresh_token",refresh_token); parameters.putIfAbsent("username",username); parameters.putIfAbsent("password",password); OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(request.getUserPrincipal(), parameters).getBody(); Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder() .token(oAuth2AccessToken.getValue()) .refreshToken(oAuth2AccessToken.getRefreshToken().getValue()) .expiresIn(oAuth2AccessToken.getExpiresIn()) .tokenHead(AuthConstant.JWT_TOKEN_PREFIX).build(); return CommonResult.success(oauth2TokenDto); } }
獲取RSA公鑰接口:
@RestController @Api(tags = "KeyPairController", description = "獲取RSA公鑰接口") @RequestMapping("/rsa") public class KeyPairController { @Autowired private KeyPair keyPair; @GetMapping("/publicKey") public Map<String, Object> getKey() { RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); } }
Oauth2獲取Token返回信息封裝:
@Data @EqualsAndHashCode(callSuper = false) @Builder public class Oauth2TokenDto { @ApiModelProperty("訪問令牌") private String token; @ApiModelProperty("刷令牌") private String refreshToken; @ApiModelProperty("訪問令牌頭前綴") private String tokenHead; @ApiModelProperty("有效時間(秒)") private int expiresIn; }
登錄用戶信息:
@Data public class SecurityUser implements UserDetails { /** * ID */ private Long id; /** * 用戶名 */ private String username; /** * 用戶密碼 */ private String password; /** * 用戶狀態(tài) */ private Boolean enabled; /** * 登錄客戶端ID */ private String clientId; /** * 權(quán)限數(shù)據(jù) */ private Collection<SimpleGrantedAuthority> authorities; public SecurityUser() { } public SecurityUser(UserDto userDto) { this.setId(userDto.getId()); this.setUsername(userDto.getUsername()); this.setPassword(userDto.getPassword()); this.setEnabled(userDto.getStatus() == 1); this.setClientId(userDto.getClientId()); if (userDto.getRoles() != null) { authorities = new ArrayList<>(); userDto.getRoles().forEach(item -> authorities.add(new SimpleGrantedAuthority(item))); } } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @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.enabled; } }
全局處理Oauth2拋出的異常:
@ControllerAdvice public class Oauth2ExceptionHandler { @ResponseBody @ExceptionHandler(value = OAuth2Exception.class) public CommonResult handleOauth2(OAuth2Exception e) { return CommonResult.failed(e.getMessage()); } }
后臺用戶服務遠程調(diào)用Service:
@FeignClient("admin") public interface UmsAdminService { @GetMapping("/admin/loadByUsername") UserDto loadUserByUsername(@RequestParam String username); }
用戶管理業(yè)務類:
@Service public class UserServiceImpl implements UserDetailsService { @Autowired private UmsAdminService adminService; @Autowired private HttpServletRequest request; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String clientId = request.getParameter("client_id"); UserDto userDto = null; if(AuthConstant.ADMIN_CLIENT_ID.equals(clientId)){ userDto = adminService.loadUserByUsername(username); } if (userDto==null) { throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); } userDto.setClientId(clientId); SecurityUser securityUser = new SecurityUser(userDto); if (!securityUser.isEnabled()) { throw new DisabledException(MessageConstant.ACCOUNT_DISABLED); } else if (!securityUser.isAccountNonLocked()) { throw new LockedException(MessageConstant.ACCOUNT_LOCKED); } else if (!securityUser.isAccountNonExpired()) { throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED); } else if (!securityUser.isCredentialsNonExpired()) { throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED); } return securityUser; } }
yml配置:
spring: mvc: pathmatch: matching-strategy: ant_path_matcher management: endpoints: web: exposure: include: "*" feign: okhttp: enabled: true client: config: default: connectTimeout: 5000 readTimeout: 5000 loggerLevel: basic logging: level: root: info #spring: # application: # name: oauth # cloud: # nacos: # config: # server-addr: 127.0.0.1:8848 # username: nacos # password: nacos # namespace: 4e903430-c64b-4c68-a43c-59478dd173e6 # group: DEFAULT_GROUP # prefix: ${spring.application.name} # file-extension: yaml
jwt.jks需要自己去生成
4.common代碼部分
通用返回對象:
public class CommonResult<T> { private long code; private String message; private T data; protected CommonResult() { } protected CommonResult(long code, String message, T data) { this.code = code; this.message = message; this.data = data; } /** * 成功返回結(jié)果 * * @param data 獲取的數(shù)據(jù) */ public static <T> CommonResult<T> success(T data) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); } /** * 成功返回結(jié)果 * * @param data 獲取的數(shù)據(jù) * @param message 提示信息 */ public static <T> CommonResult<T> success(T data, String message) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data); } /** * 失敗返回結(jié)果 * @param errorCode 錯誤碼 */ public static <T> CommonResult<T> failed(IErrorCode errorCode) { return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null); } /** * 失敗返回結(jié)果 * @param errorCode 錯誤碼 * @param message 錯誤信息 */ public static <T> CommonResult<T> failed(IErrorCode errorCode,String message) { return new CommonResult<T>(errorCode.getCode(), message, null); } /** * 失敗返回結(jié)果 * @param message 提示信息 */ public static <T> CommonResult<T> failed(String message) { return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null); } /** * 失敗返回結(jié)果 */ public static <T> CommonResult<T> failed() { return failed(ResultCode.FAILED); } /** * 參數(shù)驗證失敗返回結(jié)果 */ public static <T> CommonResult<T> validateFailed() { return failed(ResultCode.VALIDATE_FAILED); } /** * 參數(shù)驗證失敗返回結(jié)果 * @param message 提示信息 */ public static <T> CommonResult<T> validateFailed(String message) { return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null); } /** * 未登錄返回結(jié)果 */ public static <T> CommonResult<T> unauthorized(T data) { return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); } /** * 未授權(quán)返回結(jié)果 */ public static <T> CommonResult<T> forbidden(T data) { return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); } public long getCode() { return code; } public void setCode(long code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
封裝API的錯誤碼:
public interface IErrorCode { long getCode(); String getMessage(); }
枚舉了一些常用API操作碼:
public enum ResultCode implements IErrorCode { SUCCESS(200, "操作成功"), FAILED(500, "操作失敗"), VALIDATE_FAILED(404, "參數(shù)檢驗失敗"), UNAUTHORIZED(401, "暫未登錄或token已經(jīng)過期"), FORBIDDEN(403, "沒有相關(guān)權(quán)限"); private long code; private String message; private ResultCode(long code, String message) { this.code = code; this.message = message; } public long getCode() { return code; } public String getMessage() { return message; } }
Redis基礎(chǔ)配置:
public class BaseRedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisSerializer<Object> serializer = redisSerializer(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(serializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(serializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean public RedisSerializer<Object> redisSerializer() { //創(chuàng)建JSON序列化器 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); //必須設(shè)置,否則無法將JSON轉(zhuǎn)化為對象,會轉(zhuǎn)化成Map類型 objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(objectMapper); return serializer; } @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); //設(shè)置Redis緩存有效期為1天 RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1)); return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); } @Bean public RedisService redisService(){ return new RedisServiceImpl(); } }
Swagger基礎(chǔ)配置:
public abstract class BaseSwaggerConfig { @Bean public Docket createRestApi() { SwaggerProperties swaggerProperties = swaggerProperties(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo(swaggerProperties)) .select() .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getApiBasePackage())) .paths(PathSelectors.any()) .build(); if (swaggerProperties.isEnableSecurity()) { docket.securitySchemes(securitySchemes()).securityContexts(securityContexts()); } return docket; } private ApiInfo apiInfo(SwaggerProperties swaggerProperties) { return new ApiInfoBuilder() .title(swaggerProperties.getTitle()) .description(swaggerProperties.getDescription()) .contact(new Contact(swaggerProperties.getContactName(), swaggerProperties.getContactUrl(), swaggerProperties.getContactEmail())) .version(swaggerProperties.getVersion()) .build(); } private List<SecurityScheme> securitySchemes() { //設(shè)置請求頭信息 List<SecurityScheme> result = new ArrayList<>(); ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header"); result.add(apiKey); return result; } private List<SecurityContext> securityContexts() { //設(shè)置需要登錄認證的路徑 List<SecurityContext> result = new ArrayList<>(); result.add(getContextByPath("/*/.*")); return result; } private SecurityContext getContextByPath(String pathRegex) { return SecurityContext.builder() .securityReferences(defaultAuth()) .forPaths(PathSelectors.regex(pathRegex)) .build(); } private List<SecurityReference> defaultAuth() { List<SecurityReference> result = new ArrayList<>(); AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; authorizationScopes[0] = authorizationScope; result.add(new SecurityReference("Authorization", authorizationScopes)); return result; } public BeanPostProcessor generateBeanPostProcessor() { return new BeanPostProcessor() { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) { customizeSpringfoxHandlerMappings(getHandlerMappings(bean)); } return bean; } private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) { List<T> copy = mappings.stream() .filter(mapping -> mapping.getPatternParser() == null) .collect(Collectors.toList()); mappings.clear(); mappings.addAll(copy); } @SuppressWarnings("unchecked") private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) { try { Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings"); field.setAccessible(true); return (List<RequestMappingInfoHandlerMapping>) field.get(bean); } catch (IllegalArgumentException | IllegalAccessException e) { throw new IllegalStateException(e); } } }; } /** * 自定義Swagger配置 */ public abstract SwaggerProperties swaggerProperties(); }
權(quán)限相關(guān)常量定義:
public interface AuthConstant { /** * JWT存儲權(quán)限前綴 */ String AUTHORITY_PREFIX = "ROLE_"; /** * JWT存儲權(quán)限屬性 */ String AUTHORITY_CLAIM_NAME = "authorities"; /** * 后臺 client_id */ String ADMIN_CLIENT_ID = "admin-app"; /** * app client_id */ String PORTAL_CLIENT_ID = "portal-app"; /** * 后臺接口路徑匹配 */ String ADMIN_URL_PATTERN = "/admin/**"; /** * Redis緩存權(quán)限規(guī)則key */ String RESOURCE_ROLES_MAP_KEY = "auth:resourceRolesMap"; /** * 認證信息Http請求頭 */ String JWT_TOKEN_HEADER = "Authorization"; /** * JWT令牌前綴 */ String JWT_TOKEN_PREFIX = "Bearer "; /** * 用戶信息Http請求頭 */ String USER_TOKEN_HEADER = "user"; }
Swagger自定義配置:
@Data @EqualsAndHashCode(callSuper = false) @Builder public class SwaggerProperties { /** * API文檔生成基礎(chǔ)路徑 */ private String apiBasePackage; /** * 是否要啟用登錄認證 */ private boolean enableSecurity; /** * 文檔標題 */ private String title; /** * 文檔描述 */ private String description; /** * 文檔版本 */ private String version; /** * 文檔聯(lián)系人姓名 */ private String contactName; /** * 文檔聯(lián)系人網(wǎng)址 */ private String contactUrl; /** * 文檔聯(lián)系人郵箱 */ private String contactEmail; }
登錄用戶信息:
@Data @EqualsAndHashCode(callSuper = false) @NoArgsConstructor public class UserDto { private Long id; private String username; private String password; private Integer status; private String clientId; private List<String> roles; }
Controller層的日志封裝類:
@Data @EqualsAndHashCode(callSuper = false) public class WebLog { /** * 操作描述 */ private String description; /** * 操作用戶 */ private String username; /** * 操作時間 */ private Long startTime; /** * 消耗時間 */ private Integer spendTime; /** * 根路徑 */ private String basePath; /** * URI */ private String uri; /** * URL */ private String url; /** * 請求類型 */ private String method; /** * IP地址 */ private String ip; /** * 請求參數(shù) */ private Object parameter; /** * 返回結(jié)果 */ private Object result; }
自定義API異常:
public class ApiException extends RuntimeException { private IErrorCode errorCode; public ApiException(IErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } public ApiException(String message) { super(message); } public ApiException(Throwable cause) { super(cause); } public ApiException(String message, Throwable cause) { super(message, cause); } public IErrorCode getErrorCode() { return errorCode; } }
斷言處理類,用于拋出各種API異常:
public class Asserts { public static void fail(String message) { throw new ApiException(message); } public static void fail(IErrorCode errorCode) { throw new ApiException(errorCode); } }
全局異常處理:
@ControllerAdvice public class GlobalExceptionHandler { @ResponseBody @ExceptionHandler(value = ApiException.class) public CommonResult handle(ApiException e) { if (e.getErrorCode() != null) { return CommonResult.failed(e.getErrorCode()); } return CommonResult.failed(e.getMessage()); } @ResponseBody @ExceptionHandler(value = MethodArgumentNotValidException.class) public CommonResult handleValidException(MethodArgumentNotValidException e) { BindingResult bindingResult = e.getBindingResult(); String message = null; if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); if (fieldError != null) { message = fieldError.getField()+fieldError.getDefaultMessage(); } } return CommonResult.validateFailed(message); } @ResponseBody @ExceptionHandler(value = BindException.class) public CommonResult handleValidException(BindException e) { BindingResult bindingResult = e.getBindingResult(); String message = null; if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); if (fieldError != null) { message = fieldError.getField()+fieldError.getDefaultMessage(); } } return CommonResult.validateFailed(message); } }
統(tǒng)一日志處理切面:
@Aspect @Component @Order(1) public class WebLogAspect { private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class); @Pointcut("execution(public * com.*.*.controller.*.*(..))||execution(public * com.*.*.controller.*.*(..))") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { } @AfterReturning(value = "webLog()", returning = "ret") public void doAfterReturning(Object ret) throws Throwable { } @Around("webLog()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); //獲取當前請求對象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); //記錄請求信息(通過Logstash傳入Elasticsearch) WebLog webLog = new WebLog(); Object result = joinPoint.proceed(); Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if (method.isAnnotationPresent(ApiOperation.class)) { ApiOperation log = method.getAnnotation(ApiOperation.class); webLog.setDescription(log.value()); } long endTime = System.currentTimeMillis(); String urlStr = request.getRequestURL().toString(); webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath())); webLog.setIp(request.getRemoteUser()); webLog.setMethod(request.getMethod()); webLog.setParameter(getParameter(method, joinPoint.getArgs())); webLog.setResult(result); webLog.setSpendTime((int) (endTime - startTime)); webLog.setStartTime(startTime); webLog.setUri(request.getRequestURI()); webLog.setUrl(request.getRequestURL().toString()); Map<String,Object> logMap = new HashMap<>(); logMap.put("url",webLog.getUrl()); logMap.put("method",webLog.getMethod()); logMap.put("parameter",webLog.getParameter()); logMap.put("spendTime",webLog.getSpendTime()); logMap.put("description",webLog.getDescription()); // LOGGER.info("{}", JSONUtil.parse(webLog)); LOGGER.info(Markers.appendEntries(logMap), JSONUtil.parse(webLog).toString()); return result; } /** * 根據(jù)方法和傳入的參數(shù)獲取請求參數(shù) */ private Object getParameter(Method method, Object[] args) { List<Object> argList = new ArrayList<>(); Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { //將RequestBody注解修飾的參數(shù)作為請求參數(shù) RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class); if (requestBody != null) { argList.add(args[i]); } //將RequestParam注解修飾的參數(shù)作為請求參數(shù) RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class); if (requestParam != null) { Map<String, Object> map = new HashMap<>(); String key = parameters[i].getName(); if (!StringUtils.isEmpty(requestParam.value())) { key = requestParam.value(); } map.put(key, args[i]); argList.add(map); } } if (argList.size() == 0) { return null; } else if (argList.size() == 1) { return argList.get(0); } else { return argList; } } }
redis操作Service:
public interface RedisService { /** * 保存屬性 */ void set(String key, Object value, long time); /** * 保存屬性 */ void set(String key, Object value); /** * 獲取屬性 */ Object get(String key); /** * 刪除屬性 */ Boolean del(String key); /** * 批量刪除屬性 */ Long del(List<String> keys); /** * 設(shè)置過期時間 */ Boolean expire(String key, long time); /** * 獲取過期時間 */ Long getExpire(String key); /** * 判斷是否有該屬性 */ Boolean hasKey(String key); /** * 按delta遞增 */ Long incr(String key, long delta); /** * 按delta遞減 */ Long decr(String key, long delta); /** * 獲取Hash結(jié)構(gòu)中的屬性 */ Object hGet(String key, String hashKey); /** * 向Hash結(jié)構(gòu)中放入一個屬性 */ Boolean hSet(String key, String hashKey, Object value, long time); /** * 向Hash結(jié)構(gòu)中放入一個屬性 */ void hSet(String key, String hashKey, Object value); /** * 直接獲取整個Hash結(jié)構(gòu) */ Map<Object, Object> hGetAll(String key); /** * 直接設(shè)置整個Hash結(jié)構(gòu) */ Boolean hSetAll(String key, Map<String, Object> map, long time); /** * 直接設(shè)置整個Hash結(jié)構(gòu) */ void hSetAll(String key, Map<String, ?> map); /** * 刪除Hash結(jié)構(gòu)中的屬性 */ void hDel(String key, Object... hashKey); /** * 判斷Hash結(jié)構(gòu)中是否有該屬性 */ Boolean hHasKey(String key, String hashKey); /** * Hash結(jié)構(gòu)中屬性遞增 */ Long hIncr(String key, String hashKey, Long delta); /** * Hash結(jié)構(gòu)中屬性遞減 */ Long hDecr(String key, String hashKey, Long delta); /** * 獲取Set結(jié)構(gòu) */ Set<Object> sMembers(String key); /** * 向Set結(jié)構(gòu)中添加屬性 */ Long sAdd(String key, Object... values); /** * 向Set結(jié)構(gòu)中添加屬性 */ Long sAdd(String key, long time, Object... values); /** * 是否為Set中的屬性 */ Boolean sIsMember(String key, Object value); /** * 獲取Set結(jié)構(gòu)的長度 */ Long sSize(String key); /** * 刪除Set結(jié)構(gòu)中的屬性 */ Long sRemove(String key, Object... values); /** * 獲取List結(jié)構(gòu)中的屬性 */ List<Object> lRange(String key, long start, long end); /** * 獲取List結(jié)構(gòu)的長度 */ Long lSize(String key); /** * 根據(jù)索引獲取List中的屬性 */ Object lIndex(String key, long index); /** * 向List結(jié)構(gòu)中添加屬性 */ Long lPush(String key, Object value); /** * 向List結(jié)構(gòu)中添加屬性 */ Long lPush(String key, Object value, long time); /** * 向List結(jié)構(gòu)中批量添加屬性 */ Long lPushAll(String key, Object... values); /** * 向List結(jié)構(gòu)中批量添加屬性 */ Long lPushAll(String key, Long time, Object... values); /** * 從List結(jié)構(gòu)中移除屬性 */ Long lRemove(String key, long count, Object value); }
redis操作實現(xiàn)類:
public class RedisServiceImpl implements RedisService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public void set(String key, Object value, long time) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } @Override public void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); } @Override public Object get(String key) { return redisTemplate.opsForValue().get(key); } @Override public Boolean del(String key) { return redisTemplate.delete(key); } @Override public Long del(List<String> keys) { return redisTemplate.delete(keys); } @Override public Boolean expire(String key, long time) { return redisTemplate.expire(key, time, TimeUnit.SECONDS); } @Override public Long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } @Override public Boolean hasKey(String key) { return redisTemplate.hasKey(key); } @Override public Long incr(String key, long delta) { return redisTemplate.opsForValue().increment(key, delta); } @Override public Long decr(String key, long delta) { return redisTemplate.opsForValue().increment(key, -delta); } @Override public Object hGet(String key, String hashKey) { return redisTemplate.opsForHash().get(key, hashKey); } @Override public Boolean hSet(String key, String hashKey, Object value, long time) { redisTemplate.opsForHash().put(key, hashKey, value); return expire(key, time); } @Override public void hSet(String key, String hashKey, Object value) { redisTemplate.opsForHash().put(key, hashKey, value); } @Override public Map<Object, Object> hGetAll(String key) { return redisTemplate.opsForHash().entries(key); } @Override public Boolean hSetAll(String key, Map<String, Object> map, long time) { redisTemplate.opsForHash().putAll(key, map); return expire(key, time); } @Override public void hSetAll(String key, Map<String, ?> map) { redisTemplate.opsForHash().putAll(key, map); } @Override public void hDel(String key, Object... hashKey) { redisTemplate.opsForHash().delete(key, hashKey); } @Override public Boolean hHasKey(String key, String hashKey) { return redisTemplate.opsForHash().hasKey(key, hashKey); } @Override public Long hIncr(String key, String hashKey, Long delta) { return redisTemplate.opsForHash().increment(key, hashKey, delta); } @Override public Long hDecr(String key, String hashKey, Long delta) { return redisTemplate.opsForHash().increment(key, hashKey, -delta); } @Override public Set<Object> sMembers(String key) { return redisTemplate.opsForSet().members(key); } @Override public Long sAdd(String key, Object... values) { return redisTemplate.opsForSet().add(key, values); } @Override public Long sAdd(String key, long time, Object... values) { Long count = redisTemplate.opsForSet().add(key, values); expire(key, time); return count; } @Override public Boolean sIsMember(String key, Object value) { return redisTemplate.opsForSet().isMember(key, value); } @Override public Long sSize(String key) { return redisTemplate.opsForSet().size(key); } @Override public Long sRemove(String key, Object... values) { return redisTemplate.opsForSet().remove(key, values); } @Override public List<Object> lRange(String key, long start, long end) { return redisTemplate.opsForList().range(key, start, end); } @Override public Long lSize(String key) { return redisTemplate.opsForList().size(key); } @Override public Object lIndex(String key, long index) { return redisTemplate.opsForList().index(key, index); } @Override public Long lPush(String key, Object value) { return redisTemplate.opsForList().rightPush(key, value); } @Override public Long lPush(String key, Object value, long time) { Long index = redisTemplate.opsForList().rightPush(key, value); expire(key, time); return index; } @Override public Long lPushAll(String key, Object... values) { return redisTemplate.opsForList().rightPushAll(key, values); } @Override public Long lPushAll(String key, Long time, Object... values) { Long count = redisTemplate.opsForList().rightPushAll(key, values); expire(key, time); return count; } @Override public Long lRemove(String key, long count, Object value) { return redisTemplate.opsForList().remove(key, count, value); } }
logback-spring.xml配置:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration> <configuration> <!--引用默認日志配置--> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <!--使用默認的控制臺日志輸出實現(xiàn)--> <include resource="org/springframework/boot/logging/logback/console-appender.xml"/> <!--應用名稱--> <springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="springBoot"/> <!--日志文件保存路徑--> <property name="LOG_FILE_PATH" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/logs}"/> <!--LogStash訪問host--> <springProperty name="LOG_STASH_HOST" scope="context" source="logstash.host" defaultValue="localhost"/> <!--DEBUG日志輸出到文件--> <appender name="FILE_DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--輸出DEBUG以上級別日志--> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>DEBUG</level> </filter> <encoder> <!--設(shè)置為默認的文件日志格式--> <pattern>${FILE_LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!--設(shè)置文件命名格式--> <fileNamePattern>${LOG_FILE_PATH}/debug/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern> <!--設(shè)置日志文件大小,超過就重新生成文件,默認10M--> <maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize> <!--日志文件保留天數(shù),默認30天--> <maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory> </rollingPolicy> </appender> <!--ERROR日志輸出到文件--> <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--只輸出ERROR級別的日志--> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <encoder> <!--設(shè)置為默認的文件日志格式--> <pattern>${FILE_LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!--設(shè)置文件命名格式--> <fileNamePattern>${LOG_FILE_PATH}/error/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern> <!--設(shè)置日志文件大小,超過就重新生成文件,默認10M--> <maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize> <!--日志文件保留天數(shù),默認30天--> <maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory> </rollingPolicy> </appender> <!--DEBUG日志輸出到LogStash--> <appender name="LOG_STASH_DEBUG" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>DEBUG</level> </filter> <destination>${LOG_STASH_HOST}:4560</destination> <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp> <timeZone>Asia/Shanghai</timeZone> </timestamp> <!--自定義日志輸出格式--> <pattern> <pattern> { "project": "mall-swarm", "level": "%level", "service": "${APP_NAME:-}", "pid": "${PID:-}", "thread": "%thread", "class": "%logger", "message": "%message", "stack_trace": "%exception{20}" } </pattern> </pattern> </providers> </encoder> <!--當有多個LogStash服務時,設(shè)置訪問策略為輪詢--> <connectionStrategy> <roundRobin> <connectionTTL>5 minutes</connectionTTL> </roundRobin> </connectionStrategy> </appender> <!--ERROR日志輸出到LogStash--> <appender name="LOG_STASH_ERROR" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <destination>${LOG_STASH_HOST}:4561</destination> <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp> <timeZone>Asia/Shanghai</timeZone> </timestamp> <!--自定義日志輸出格式--> <pattern> <pattern> { "project": "mall-swarm", "level": "%level", "service": "${APP_NAME:-}", "pid": "${PID:-}", "thread": "%thread", "class": "%logger", "message": "%message", "stack_trace": "%exception{20}" } </pattern> </pattern> </providers> </encoder> <!--當有多個LogStash服務時,設(shè)置訪問策略為輪詢--> <connectionStrategy> <roundRobin> <connectionTTL>5 minutes</connectionTTL> </roundRobin> </connectionStrategy> </appender> <!--業(yè)務日志輸出到LogStash--> <appender name="LOG_STASH_BUSINESS" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <destination>${LOG_STASH_HOST}:4562</destination> <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp> <timeZone>Asia/Shanghai</timeZone> </timestamp> <!--自定義日志輸出格式--> <pattern> <pattern> { "project": "mall-swarm", "level": "%level", "service": "${APP_NAME:-}", "pid": "${PID:-}", "thread": "%thread", "class": "%logger", "message": "%message", "stack_trace": "%exception{20}" } </pattern> </pattern> </providers> </encoder> <!--當有多個LogStash服務時,設(shè)置訪問策略為輪詢--> <connectionStrategy> <roundRobin> <connectionTTL>5 minutes</connectionTTL> </roundRobin> </connectionStrategy> </appender> <!--接口訪問記錄日志輸出到LogStash--> <appender name="LOG_STASH_RECORD" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <destination>${LOG_STASH_HOST}:4563</destination> <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp> <timeZone>Asia/Shanghai</timeZone> </timestamp> <!--自定義日志輸出格式--> <pattern> <pattern> { "project": "mall-swarm", "level": "%level", "service": "${APP_NAME:-}", "class": "%logger", "message": "%message" } </pattern> </pattern> </providers> </encoder> <!--當有多個LogStash服務時,設(shè)置訪問策略為輪詢--> <connectionStrategy> <roundRobin> <connectionTTL>5 minutes</connectionTTL> </roundRobin> </connectionStrategy> </appender> <!--控制框架輸出日志--> <logger name="org.slf4j" level="INFO"/> <logger name="springfox" level="INFO"/> <logger name="io.swagger" level="INFO"/> <logger name="org.springframework" level="INFO"/> <logger name="org.hibernate.validator" level="INFO"/> <logger name="com.alibaba.nacos.client.naming" level="INFO"/> <root level="DEBUG"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE_DEBUG"/> <appender-ref ref="FILE_ERROR"/> <appender-ref ref="LOG_STASH_DEBUG"/> <appender-ref ref="LOG_STASH_ERROR"/> </root> <logger name="com.*.*.common.log.WebLogAspect" level="DEBUG"> <appender-ref ref="LOG_STASH_RECORD"/> </logger> <logger name="com.*.*" level="DEBUG"> <appender-ref ref="LOG_STASH_BUSINESS"/> </logger> </configuration>
5.admin代碼部分
Swagger API文檔相關(guān)配置:
@Configuration @EnableSwagger2 public class SwaggerConfig extends BaseSwaggerConfig { @Override public SwaggerProperties swaggerProperties() { return SwaggerProperties.builder() .apiBasePackage("com.admin.controller") .title("后臺系統(tǒng)") .description("后臺相關(guān)接口文檔") .contactName("admin") .version("1.0") .enableSecurity(true) .build(); } @Bean public BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { return generateBeanPostProcessor(); } }
后臺用戶管理:
@Controller @Api(tags = "UmsAdminController", description = "后臺用戶管理") @RequestMapping("/admin") public class UmsAdminController { @Autowired private UmsAdminService adminService; @ApiOperation(value = "用戶注冊") @RequestMapping(value = "/register", method = RequestMethod.POST) @ResponseBody public CommonResult<UmsAdmin> register(@Validated @RequestBody UmsAdminParam umsAdminParam) { UmsAdmin umsAdmin = adminService.register(umsAdminParam); if (umsAdmin == null) { return CommonResult.failed(); } return CommonResult.success(umsAdmin); } @ApiOperation(value = "登錄以后返回token") @RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public CommonResult login(@Validated @RequestBody UmsAdminLoginParam umsAdminLoginParam) { return adminService.login(umsAdminLoginParam.getUsername(),umsAdminLoginParam.getPassword()); } @ApiOperation(value = "登出功能") @RequestMapping(value = "/logout", method = RequestMethod.POST) @ResponseBody public CommonResult logout() { return CommonResult.success(null); } @ApiOperation("根據(jù)用戶名獲取通用用戶信息") @RequestMapping(value = "/loadByUsername", method = RequestMethod.GET) @ResponseBody public UserDto loadUserByUsername(@RequestParam String username) { UserDto userDTO = adminService.loadUserByUsername(username); return userDTO; } }
UmsAdminService實現(xiàn)類:
@Service public class UmsAdminServiceImpl implements UmsAdminService { private static final Logger LOGGER = LoggerFactory.getLogger(UmsAdminServiceImpl.class); @Autowired private UmsAdminMapper adminMapper; @Autowired private UmsAdminLoginLogMapper loginLogMapper; @Autowired private AuthService authService; @Autowired private HttpServletRequest request; @Override public UmsAdmin register(UmsAdminParam umsAdminParam) { UmsAdmin umsAdmin = new UmsAdmin(); BeanUtils.copyProperties(umsAdminParam, umsAdmin); umsAdmin.setCreateTime(new Date()); umsAdmin.setStatus(1); //查詢是否有相同用戶名的用戶 List<UmsAdmin> umsAdminList = adminMapper.selectList(new QueryWrapper<UmsAdmin>().eq("username",umsAdmin.getUsername())); if (umsAdminList.size() > 0) { return null; } //將密碼進行加密操作 String encodePassword = BCrypt.hashpw(umsAdmin.getPassword()); umsAdmin.setPassword(encodePassword); adminMapper.insert(umsAdmin); return umsAdmin; } @Override public CommonResult login(String username, String password) { if(StrUtil.isEmpty(username)||StrUtil.isEmpty(password)){ Asserts.fail("用戶名或密碼不能為空!"); } Map<String, String> params = new HashMap<>(); params.put("client_id", AuthConstant.ADMIN_CLIENT_ID); params.put("client_secret","123456"); params.put("grant_type","password"); params.put("username",username); params.put("password",password); CommonResult restResult = authService.getAccessToken(params); if(ResultCode.SUCCESS.getCode()==restResult.getCode()&&restResult.getData()!=null){ } return restResult; } @Override public UserDto loadUserByUsername(String username) { //獲取用戶信息 UmsAdmin admin = getAdminByUsername(username); if (admin != null) { UserDto userDTO = new UserDto(); BeanUtils.copyProperties(admin,userDTO); return userDTO; } return null; } @Override public UmsAdmin getAdminByUsername(String username) { List<UmsAdmin> adminList = adminMapper.selectList(new QueryWrapper<UmsAdmin>().eq("username",username)); if (adminList != null && adminList.size() > 0) { return adminList.get(0); } return null; } }
認證服務遠程調(diào)用Service:
@FeignClient("oauth") public interface AuthService { @PostMapping(value = "/oauth/token") CommonResult getAccessToken(@RequestParam Map<String, String> parameters); }
后臺管理員Service:
public interface UmsAdminService { /** * 注冊功能 */ UmsAdmin register(UmsAdminParam umsAdminParam); /** * 登錄功能 * @param username 用戶名 * @param password 密碼 * @return 調(diào)用認證中心返回結(jié)果 */ CommonResult login(String username, String password); /** * 獲取用戶信息 */ UserDto loadUserByUsername(String username); /** * 根據(jù)用戶名獲取后臺管理員 */ UmsAdmin getAdminByUsername(String username); }
yml配置:
spring: mvc: pathmatch: matching-strategy: ant_path_matcher datasource: url: jdbc:mysql://localhost:3306/admin?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: root druid: initial-size: 5 #連接池初始化大小 min-idle: 10 #最小空閑連接數(shù) max-active: 20 #最大連接數(shù) web-stat-filter: exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" #不統(tǒng)計這些請求數(shù)據(jù) stat-view-servlet: #訪問監(jiān)控網(wǎng)頁的登錄用戶名和密碼 login-username: druid login-password: druid redis: host: localhost # Redis服務器地址 database: 0 # Redis數(shù)據(jù)庫索引(默認為0) port: 6379 # Redis服務器連接端口 password: # Redis服務器連接密碼(默認為空) timeout: 3000ms # 連接超時時間(毫秒) management: #開啟SpringBoot Admin的監(jiān)控 endpoints: web: exposure: include: '*' endpoint: health: show-details: always redis: database: admin key: admin: 'ums:admin' expire: common: 86400 # 24小時 feign: okhttp: enabled: true client: config: default: connectTimeout: 5000 readTimeout: 5000 loggerLevel: basic mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
總結(jié)
本人第一次寫文章,如有看不懂的多多包涵
以上就是今天要講的內(nèi)容,本文僅僅簡單介紹了統(tǒng)一認證的使用,后續(xù)會把源碼地址分享出來。
項目地址:https://gitee.com/zhouwudi/SpringSecurityOauth2
到此這篇關(guān)于Spring Cloud 完整整合 Security + Oauth2 + jwt實現(xiàn)權(quán)限認證的文章就介紹到這了,更多相關(guān)Spring Cloud Security + Oauth2 + jwt權(quán)限認證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java數(shù)據(jù)導出功能之導出Excel文件實例
這篇文章主要介紹了Java數(shù)據(jù)導出功能之導出Excel文件實例,本文給出了jar包的下載地址,并給出了導出Excel文件代碼實例,需要的朋友可以參考下2015-06-06淺談SpringBoot之開啟數(shù)據(jù)庫遷移的FlyWay使用
這篇文章主要介紹了淺談SpringBoot之開啟數(shù)據(jù)庫遷移的FlyWay使用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01關(guān)于spring?boot使用?jdbc+mysql?連接的問題
這篇文章主要介紹了spring?boot使用?jdbc+mysql?連接,在這里mysql?8.x版本驅(qū)動包,要使用?com.mysql.cj.jdbc.Driver作為驅(qū)動類,文中給大家詳細介紹,需要的朋友可以參考下2022-03-03如何通過ServletInputStream讀取http請求傳入的數(shù)據(jù)
這篇文章主要介紹了如何通過ServletInputStream讀取http請求傳入的數(shù)據(jù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10詳解Java8新特性Stream之list轉(zhuǎn)map及問題解決
這篇文章主要介紹了詳解Java8新特性Stream之list轉(zhuǎn)map及問題解決,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-09-09Spring?Boot實現(xiàn)web.xml功能示例詳解
這篇文章主要介紹了Spring?Boot實現(xiàn)web.xml功能,通過本文介紹我們了解到,在Spring Boot應用中,我們可以通過注解和編程兩種方式實現(xiàn)web.xml的功能,包括如何創(chuàng)建及注冊Servlet、Filter以及Listener等,需要的朋友可以參考下2023-09-09