Spring Cloud 完整整合 Security + Oauth2 + jwt实现权限认证
文章目录
前言
OAuth2是一个开放的标准,协议。即允许用户让第三方应用访问某一个网站上存储的用户私密资源(照片,头像等)。这个过程中无需将用户名和密码提供给第三方应用。在互联网中,我们最常见的OAuth2的应用就是各种第三方通过QQ授权,微信授权,微博授权等登录了。
OAuth2协议一共支持4中不同的授权模式。
- 授权码模式:常见的第三方平台登录功能基本上都使用这种模式。
- 简化模式:简化模式不想要客户端服务器(第三方应用服务器)参与,直接在浏览器中向授权服务器申请令牌。
- 密码模式:密码模式是用户把用户名,密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌。这需要用户对客户端高度信任。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务器申请授权。
OAuth2的重要参数
①response_type
code:表示要求返回授权码。token:表示直接返回令牌
②client_id
客户端身份标识
③client_secret
客户端密钥
④redirect_uri
重定向地址
⑤scope
表示授权的范围。read:只读权限,all读写权限
⑥grant_type
表示授权的方式。AUTHORIZATION_CODE(授权码),PASSWORD(密码),CLIENT_CREDENTIALS(品正式),REFRESH_TOKEN(更新令牌)
⑦state
应用程序传递的一个随机数,用来防止CSRF攻击
话不多说直接上代码
一、创建项目
首先创建一个主体:
SpringSecurityOauth2jtw
子项目:
admin——登录
oauth2——权限
common——公共
gateway——网关
二、步骤
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数据库驱动--><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 相关依赖--><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 相关依赖--><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代码部分
代码如下(示例):
配置鉴权管理器
@ComponentpublicclassAuthorizationManagerimplementsReactiveAuthorizationManager<AuthorizationContext>{@AutowiredprivateRedisTemplate<String,Object> redisTemplate;@AutowiredprivateIgnoreUrlsConfig ignoreUrlsConfig;@OverridepublicMono<AuthorizationDecision>check(Mono<Authentication> mono,AuthorizationContext authorizationContext){ServerHttpRequest request = authorizationContext.getExchange().getRequest();URI uri = request.getURI();PathMatcher pathMatcher =newAntPathMatcher();//白名单路径直接放行List<String> ignoreUrls = ignoreUrlsConfig.getUrls();for(String ignoreUrl : ignoreUrls){if(pathMatcher.match(ignoreUrl, uri.getPath())){returnMono.just(newAuthorizationDecision(true));}}//对应跨域的预检请求直接放行if(request.getMethod()==HttpMethod.OPTIONS){returnMono.just(newAuthorizationDecision(true));}//不同用户体系登录不允许互相访问try{String token = request.getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);if(StrUtil.isEmpty(token)){returnMono.just(newAuthorizationDecision(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())){returnMono.just(newAuthorizationDecision(false));}if(AuthConstant.PORTAL_CLIENT_ID.equals(userDto.getClientId())&& pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())){returnMono.just(newAuthorizationDecision(false));}}catch(ParseException e){
e.printStackTrace();returnMono.just(newAuthorizationDecision(false));}//非管理端路径直接放行if(!pathMatcher.match(AuthConstant.ADMIN_URL_PATTERN, uri.getPath())){returnMono.just(newAuthorizationDecision(true));}//管理端路径需校验权限Map<Object,Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstant.RESOURCE_ROLES_MAP_KEY);Iterator<Object> iterator = resourceRolesMap.keySet().iterator();List<String> authorities =newArrayList<>();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(newAuthorizationDecision(false));}
未登录或者token失效时的返回结果:
@ComponentpublicclassRestAuthenticationEntryPointimplementsServerAuthenticationEntryPoint{@OverridepublicMono<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));}}
无权限访问的返回结果:
@ComponentpublicclassRestfulAccessDeniedHandlerimplementsServerAccessDeniedHandler{@OverridepublicMono<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));}}
全局跨域配置:注意:前端从网关进行调用时需要配置
@ConfigurationpublicclassGlobalCorsConfig{@BeanpublicCorsWebFiltercorsFilter(){CorsConfiguration config =newCorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);UrlBasedCorsConfigurationSource source =newUrlBasedCorsConfigurationSource(newPathPatternParser());
source.registerCorsConfiguration("/**", config);returnnewCorsWebFilter(source);}}
网关白名单配置:
@Data@EqualsAndHashCode(callSuper =false)@Component@ConfigurationProperties(prefix="secure.ignore")publicclassIgnoreUrlsConfig{privateList<String> urls;}
资源服务器配置:
@AllArgsConstructor@Configuration@EnableWebFluxSecuritypublicclassResourceServerConfig{privatefinalAuthorizationManager authorizationManager;privatefinalIgnoreUrlsConfig ignoreUrlsConfig;privatefinalRestfulAccessDeniedHandler restfulAccessDeniedHandler;privatefinalRestAuthenticationEntryPoint restAuthenticationEntryPoint;privatefinalIgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;@BeanpublicSecurityWebFilterChainspringSecurityFilterChain(ServerHttpSecurity http){
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());//自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);//对白名单路径,直接移除JWT请求头
http.addFilterBefore(ignoreUrlsRemoveJwtFilter,SecurityWebFiltersOrder.AUTHENTICATION);
http.authorizeExchange()//白名单配置.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//鉴权管理器配置.anyExchange().access(authorizationManager).and().exceptionHandling()//处理未授权.accessDeniedHandler(restfulAccessDeniedHandler)//处理未认证.authenticationEntryPoint(restAuthenticationEntryPoint).and().csrf().disable();return http.build();}@BeanpublicConverter<Jwt,?extendsMono<?extendsAbstractAuthenticationToken>>jwtAuthenticationConverter(){JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =newJwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);JwtAuthenticationConverter jwtAuthenticationConverter =newJwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);returnnewReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);}}
Swagger资源配置:
@Slf4j@Component@Primary@AllArgsConstructorpublicclassSwaggerResourceConfigimplementsSwaggerResourcesProvider{privatefinalRouteLocator routeLocator;privatefinalGatewayProperties gatewayProperties;@OverridepublicList<SwaggerResource>get(){List<SwaggerResource> resources =newArrayList<>();List<String> routes =newArrayList<>();//获取所有路由的ID
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));//过滤出配置文件中定义的路由->过滤出Path Route Predicate->根据路径拼接成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;}privateSwaggerResourceswaggerResource(String name,String location){
log.info("name:{},location:{}", name, location);SwaggerResource swaggerResource =newSwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");return swaggerResource;}}
自定义Swagger的各个配置节点:
@RestControllerpublicclassSwaggerHandler{@Autowired(required =false)privateSecurityConfiguration securityConfiguration;@Autowired(required =false)privateUiConfiguration uiConfiguration;privatefinalSwaggerResourcesProvider swaggerResources;@AutowiredpublicSwaggerHandler(SwaggerResourcesProvider swaggerResources){this.swaggerResources = swaggerResources;}/**
* Swagger安全配置,支持oauth和apiKey设置
*/@GetMapping("/swagger-resources/configuration/security")publicMono<ResponseEntity<SecurityConfiguration>>securityConfiguration(){returnMono.just(newResponseEntity<>(Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()),HttpStatus.OK));}/**
* Swagger UI配置
*/@GetMapping("/swagger-resources/configuration/ui")publicMono<ResponseEntity<UiConfiguration>>uiConfiguration(){returnMono.just(newResponseEntity<>(Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()),HttpStatus.OK));}/**
* Swagger资源配置,微服务中这各个服务的api-docs信息
*/@GetMapping("/swagger-resources")publicMono<ResponseEntity>swaggerResources(){returnMono.just((newResponseEntity<>(swaggerResources.get(),HttpStatus.OK)));}}
将登录用户的JWT转化成用户信息的全局过滤器:
@ComponentpublicclassAuthGlobalFilterimplementsGlobalFilter,Ordered{privatestaticLogger LOGGER =LoggerFactory.getLogger(AuthGlobalFilter.class);@OverridepublicMono<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中解析用户信息并设置到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);}@OverridepublicintgetOrder(){return0;}}
白名单路径访问时移除JWT请求头的过滤器:
@ComponentpublicclassIgnoreUrlsRemoveJwtFilterimplementsWebFilter{@AutowiredprivateIgnoreUrlsConfig ignoreUrlsConfig;@OverridepublicMono<Void>filter(ServerWebExchange exchange,WebFilterChain chain){ServerHttpRequest request = exchange.getRequest();URI uri = request.getURI();PathMatcher pathMatcher =newAntPathMatcher();//白名单路径移除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:truelower-case-service-id:true#使用小写service-idroutes:#配置路由路径-id: oauth
uri: lb://oauth
predicates:- Path=/oauth/**filters:- StripPrefix=1
-id: admin
uri: lb://admin #这里是配置的nacospredicates:- 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数据库索引(默认为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的监控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
2.oauth2代码部分
JWT内容增强器:
@ComponentpublicclassJwtTokenEnhancerimplementsTokenEnhancer{@OverridepublicOAuth2AccessTokenenhance(OAuth2AccessToken accessToken,OAuth2Authentication authentication){SecurityUser securityUser =(SecurityUser) authentication.getPrincipal();Map<String,Object> info =newHashMap<>();//把用户ID设置到JWT中
info.put("id", securityUser.getId());
info.put("client_id",securityUser.getClientId());((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);return accessToken;}}
认证服务器配置:
@AllArgsConstructor@Configuration@EnableAuthorizationServerpublicclassOauth2ServerConfigextendsAuthorizationServerConfigurerAdapter{privatefinalPasswordEncoder passwordEncoder;privatefinalUserServiceImpl userDetailsService;privatefinalAuthenticationManager authenticationManager;privatefinalJwtTokenEnhancer jwtTokenEnhancer;@Overridepublicvoidconfigure(ClientDetailsServiceConfigurer clients)throwsException{
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);}@Overridepublicvoidconfigure(AuthorizationServerEndpointsConfigurer endpoints)throwsException{TokenEnhancerChain enhancerChain =newTokenEnhancerChain();List<TokenEnhancer> delegates =newArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter());
enhancerChain.setTokenEnhancers(delegates);//配置JWT的内容增强器
endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService)//配置加载用户信息的服务.accessTokenConverter(accessTokenConverter()).tokenEnhancer(enhancerChain);}@Overridepublicvoidconfigure(AuthorizationServerSecurityConfigurer security)throwsException{
security.allowFormAuthenticationForClients();}@BeanpublicJwtAccessTokenConverteraccessTokenConverter(){JwtAccessTokenConverter jwtAccessTokenConverter =newJwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());return jwtAccessTokenConverter;}@BeanpublicKeyPairkeyPair(){//从classpath下的证书中获取秘钥对KeyStoreKeyFactory keyStoreKeyFactory =newKeyStoreKeyFactory(newClassPathResource("jwt.jks"),"123456".toCharArray());return keyStoreKeyFactory.getKeyPair("jwt","123456".toCharArray());}}
Swagger相关配置:
@Configuration@EnableSwagger2publicclassSwaggerConfigextendsBaseSwaggerConfig{@OverridepublicSwaggerPropertiesswaggerProperties(){returnSwaggerProperties.builder().apiBasePackage("com.oauth.oauth2.controller").title("认证中心").description("认证中心相关接口文档").contactName("oauth").version("1.0").enableSecurity(true).build();}@BeanpublicBeanPostProcessorspringfoxHandlerProviderBeanPostProcessor(){returngenerateBeanPostProcessor();}}
SpringSecurity相关配置:
@Configuration@EnableWebSecuritypublicclassWebSecurityConfig{@BeanpublicAuthenticationManagerauthenticationManager(AuthenticationConfiguration authenticationConfiguration)throwsException{return authenticationConfiguration.getAuthenticationManager();}@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}}
消息常量定义:
publicclassMessageConstant{publicstaticfinalString LOGIN_SUCCESS ="登录成功!";publicstaticfinalString USERNAME_PASSWORD_ERROR ="用户名或密码错误!";publicstaticfinalString CREDENTIALS_EXPIRED ="该账户的登录凭证已过期,请重新登录!";publicstaticfinalString ACCOUNT_DISABLED ="该账户已被禁用,请联系管理员!";publicstaticfinalString ACCOUNT_LOCKED ="该账号已被锁定,请联系管理员!";publicstaticfinalString ACCOUNT_EXPIRED ="该账号已过期,请联系管理员!";publicstaticfinalString PERMISSION_DENIED ="没有访问权限,请联系管理员!";}
自定义Oauth2获取令牌接口:
@RestController@Api(tags ="AuthController", description ="认证中心登录认证")@RequestMapping("/oauth")publicclassAuthController{@AutowiredprivateTokenEndpoint tokenEndpoint;@ApiOperation("Oauth2获取token")@RequestMapping(value ="/token", method =RequestMethod.POST)publicCommonResult<Oauth2TokenDto>postAccessToken(HttpServletRequest request,@ApiParam("授权模式")@RequestParamString grant_type,@ApiParam("Oauth2客户端ID")@RequestParamString client_id,@ApiParam("Oauth2客户端秘钥")@RequestParamString client_secret,@ApiParam("刷新token")@RequestParam(required =false)String refresh_token,@ApiParam("登录用户名")@RequestParam(required =false)String username,@ApiParam("登录密码")@RequestParam(required =false)String password)throwsHttpRequestMethodNotSupportedException{Map<String,String> parameters =newHashMap<>();
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();returnCommonResult.success(oauth2TokenDto);}}
获取RSA公钥接口:
@RestController@Api(tags ="KeyPairController", description ="获取RSA公钥接口")@RequestMapping("/rsa")publicclassKeyPairController{@AutowiredprivateKeyPair keyPair;@GetMapping("/publicKey")publicMap<String,Object>getKey(){RSAPublicKey publicKey =(RSAPublicKey) keyPair.getPublic();RSAKey key =newRSAKey.Builder(publicKey).build();returnnewJWKSet(key).toJSONObject();}}
Oauth2获取Token返回信息封装:
@Data@EqualsAndHashCode(callSuper =false)@BuilderpublicclassOauth2TokenDto{@ApiModelProperty("访问令牌")privateString token;@ApiModelProperty("刷令牌")privateString refreshToken;@ApiModelProperty("访问令牌头前缀")privateString tokenHead;@ApiModelProperty("有效时间(秒)")privateint expiresIn;}
登录用户信息:
@DatapublicclassSecurityUserimplementsUserDetails{/**
* ID
*/privateLong id;/**
* 用户名
*/privateString username;/**
* 用户密码
*/privateString password;/**
* 用户状态
*/privateBoolean enabled;/**
* 登录客户端ID
*/privateString clientId;/**
* 权限数据
*/privateCollection<SimpleGrantedAuthority> authorities;publicSecurityUser(){}publicSecurityUser(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 =newArrayList<>();
userDto.getRoles().forEach(item -> authorities.add(newSimpleGrantedAuthority(item)));}}@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){returnthis.authorities;}@OverridepublicStringgetPassword(){returnthis.password;}@OverridepublicStringgetUsername(){returnthis.username;}@OverridepublicbooleanisAccountNonExpired(){returntrue;}@OverridepublicbooleanisAccountNonLocked(){returntrue;}@OverridepublicbooleanisCredentialsNonExpired(){returntrue;}@OverridepublicbooleanisEnabled(){returnthis.enabled;}}
全局处理Oauth2抛出的异常:
@ControllerAdvicepublicclassOauth2ExceptionHandler{@ResponseBody@ExceptionHandler(value =OAuth2Exception.class)publicCommonResulthandleOauth2(OAuth2Exception e){returnCommonResult.failed(e.getMessage());}}
后台用户服务远程调用Service:
@FeignClient("admin")publicinterfaceUmsAdminService{@GetMapping("/admin/loadByUsername")UserDtoloadUserByUsername(@RequestParamString username);}
用户管理业务类:
@ServicepublicclassUserServiceImplimplementsUserDetailsService{@AutowiredprivateUmsAdminService adminService;@AutowiredprivateHttpServletRequest request;@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{String clientId = request.getParameter("client_id");UserDto userDto =null;if(AuthConstant.ADMIN_CLIENT_ID.equals(clientId)){
userDto = adminService.loadUserByUsername(username);}if(userDto==null){thrownewUsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);}
userDto.setClientId(clientId);SecurityUser securityUser =newSecurityUser(userDto);if(!securityUser.isEnabled()){thrownewDisabledException(MessageConstant.ACCOUNT_DISABLED);}elseif(!securityUser.isAccountNonLocked()){thrownewLockedException(MessageConstant.ACCOUNT_LOCKED);}elseif(!securityUser.isAccountNonExpired()){thrownewAccountExpiredException(MessageConstant.ACCOUNT_EXPIRED);}elseif(!securityUser.isCredentialsNonExpired()){thrownewCredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED);}return securityUser;}}
yml配置:
spring:mvc:pathmatch:matching-strategy: ant_path_matcher
management:endpoints:web:exposure:include:"*"feign:okhttp:enabled:trueclient:config:default:connectTimeout:5000readTimeout:5000loggerLevel: 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需要自己去生成
2.common代码部分
通用返回对象:
publicclassCommonResult<T>{privatelong code;privateString message;privateT data;protectedCommonResult(){}protectedCommonResult(long code,String message,T data){this.code = code;this.message = message;this.data = data;}/**
* 成功返回结果
*
* @param data 获取的数据
*/publicstatic<T>CommonResult<T>success(T data){returnnewCommonResult<T>(ResultCode.SUCCESS.getCode(),ResultCode.SUCCESS.getMessage(), data);}/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/publicstatic<T>CommonResult<T>success(T data,String message){returnnewCommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);}/**
* 失败返回结果
* @param errorCode 错误码
*/publicstatic<T>CommonResult<T>failed(IErrorCode errorCode){returnnewCommonResult<T>(errorCode.getCode(), errorCode.getMessage(),null);}/**
* 失败返回结果
* @param errorCode 错误码
* @param message 错误信息
*/publicstatic<T>CommonResult<T>failed(IErrorCode errorCode,String message){returnnewCommonResult<T>(errorCode.getCode(), message,null);}/**
* 失败返回结果
* @param message 提示信息
*/publicstatic<T>CommonResult<T>failed(String message){returnnewCommonResult<T>(ResultCode.FAILED.getCode(), message,null);}/**
* 失败返回结果
*/publicstatic<T>CommonResult<T>failed(){returnfailed(ResultCode.FAILED);}/**
* 参数验证失败返回结果
*/publicstatic<T>CommonResult<T>validateFailed(){returnfailed(ResultCode.VALIDATE_FAILED);}/**
* 参数验证失败返回结果
* @param message 提示信息
*/publicstatic<T>CommonResult<T>validateFailed(String message){returnnewCommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message,null);}/**
* 未登录返回结果
*/publicstatic<T>CommonResult<T>unauthorized(T data){returnnewCommonResult<T>(ResultCode.UNAUTHORIZED.getCode(),ResultCode.UNAUTHORIZED.getMessage(), data);}/**
* 未授权返回结果
*/publicstatic<T>CommonResult<T>forbidden(T data){returnnewCommonResult<T>(ResultCode.FORBIDDEN.getCode(),ResultCode.FORBIDDEN.getMessage(), data);}publiclonggetCode(){return code;}publicvoidsetCode(long code){this.code = code;}publicStringgetMessage(){return message;}publicvoidsetMessage(String message){this.message = message;}publicTgetData(){return data;}publicvoidsetData(T data){this.data = data;}}
封装API的错误码:
publicinterfaceIErrorCode{longgetCode();StringgetMessage();}
枚举了一些常用API操作码:
publicenumResultCodeimplementsIErrorCode{SUCCESS(200,"操作成功"),FAILED(500,"操作失败"),VALIDATE_FAILED(404,"参数检验失败"),UNAUTHORIZED(401,"暂未登录或token已经过期"),FORBIDDEN(403,"没有相关权限");privatelong code;privateString message;privateResultCode(long code,String message){this.code = code;this.message = message;}publiclonggetCode(){return code;}publicStringgetMessage(){return message;}}
Redis基础配置:
publicclassBaseRedisConfig{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactory redisConnectionFactory){RedisSerializer<Object> serializer =redisSerializer();RedisTemplate<String,Object> redisTemplate =newRedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(newStringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(newStringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();return redisTemplate;}@BeanpublicRedisSerializer<Object>redisSerializer(){//创建JSON序列化器Jackson2JsonRedisSerializer<Object> serializer =newJackson2JsonRedisSerializer<>(Object.class);ObjectMapper objectMapper =newObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);//必须设置,否则无法将JSON转化为对象,会转化成Map类型
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);return serializer;}@BeanpublicRedisCacheManagerredisCacheManager(RedisConnectionFactory redisConnectionFactory){RedisCacheWriter redisCacheWriter =RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);//设置Redis缓存有效期为1天RedisCacheConfiguration redisCacheConfiguration =RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1));returnnewRedisCacheManager(redisCacheWriter, redisCacheConfiguration);}@BeanpublicRedisServiceredisService(){returnnewRedisServiceImpl();}}
Swagger基础配置:
publicabstractclassBaseSwaggerConfig{@BeanpublicDocketcreateRestApi(){SwaggerProperties swaggerProperties =swaggerProperties();Docket docket =newDocket(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;}privateApiInfoapiInfo(SwaggerProperties swaggerProperties){returnnewApiInfoBuilder().title(swaggerProperties.getTitle()).description(swaggerProperties.getDescription()).contact(newContact(swaggerProperties.getContactName(), swaggerProperties.getContactUrl(), swaggerProperties.getContactEmail())).version(swaggerProperties.getVersion()).build();}privateList<SecurityScheme>securitySchemes(){//设置请求头信息List<SecurityScheme> result =newArrayList<>();ApiKey apiKey =newApiKey("Authorization","Authorization","header");
result.add(apiKey);return result;}privateList<SecurityContext>securityContexts(){//设置需要登录认证的路径List<SecurityContext> result =newArrayList<>();
result.add(getContextByPath("/*/.*"));return result;}privateSecurityContextgetContextByPath(String pathRegex){returnSecurityContext.builder().securityReferences(defaultAuth()).forPaths(PathSelectors.regex(pathRegex)).build();}privateList<SecurityReference>defaultAuth(){List<SecurityReference> result =newArrayList<>();AuthorizationScope authorizationScope =newAuthorizationScope("global","accessEverything");AuthorizationScope[] authorizationScopes =newAuthorizationScope[1];
authorizationScopes[0]= authorizationScope;
result.add(newSecurityReference("Authorization", authorizationScopes));return result;}publicBeanPostProcessorgenerateBeanPostProcessor(){returnnewBeanPostProcessor(){@OverridepublicObjectpostProcessAfterInitialization(Object bean,String beanName)throwsBeansException{if(bean instanceofWebMvcRequestHandlerProvider|| bean instanceofWebFluxRequestHandlerProvider){customizeSpringfoxHandlerMappings(getHandlerMappings(bean));}return bean;}private<TextendsRequestMappingInfoHandlerMapping>voidcustomizeSpringfoxHandlerMappings(List<T> mappings){List<T> copy = mappings.stream().filter(mapping -> mapping.getPatternParser()==null).collect(Collectors.toList());
mappings.clear();
mappings.addAll(copy);}@SuppressWarnings("unchecked")privateList<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){thrownewIllegalStateException(e);}}};}/**
* 自定义Swagger配置
*/publicabstractSwaggerPropertiesswaggerProperties();}
权限相关常量定义:
publicinterfaceAuthConstant{/**
* JWT存储权限前缀
*/String AUTHORITY_PREFIX ="ROLE_";/**
* JWT存储权限属性
*/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缓存权限规则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)@BuilderpublicclassSwaggerProperties{/**
* API文档生成基础路径
*/privateString apiBasePackage;/**
* 是否要启用登录认证
*/privateboolean enableSecurity;/**
* 文档标题
*/privateString title;/**
* 文档描述
*/privateString description;/**
* 文档版本
*/privateString version;/**
* 文档联系人姓名
*/privateString contactName;/**
* 文档联系人网址
*/privateString contactUrl;/**
* 文档联系人邮箱
*/privateString contactEmail;}
登录用户信息:
@Data@EqualsAndHashCode(callSuper =false)@NoArgsConstructorpublicclassUserDto{privateLong id;privateString username;privateString password;privateInteger status;privateString clientId;privateList<String> roles;}
Controller层的日志封装类:
@Data@EqualsAndHashCode(callSuper =false)publicclassWebLog{/**
* 操作描述
*/privateString description;/**
* 操作用户
*/privateString username;/**
* 操作时间
*/privateLong startTime;/**
* 消耗时间
*/privateInteger spendTime;/**
* 根路径
*/privateString basePath;/**
* URI
*/privateString uri;/**
* URL
*/privateString url;/**
* 请求类型
*/privateString method;/**
* IP地址
*/privateString ip;/**
* 请求参数
*/privateObject parameter;/**
* 返回结果
*/privateObject result;}
自定义API异常:
publicclassApiExceptionextendsRuntimeException{privateIErrorCode errorCode;publicApiException(IErrorCode errorCode){super(errorCode.getMessage());this.errorCode = errorCode;}publicApiException(String message){super(message);}publicApiException(Throwable cause){super(cause);}publicApiException(String message,Throwable cause){super(message, cause);}publicIErrorCodegetErrorCode(){return errorCode;}}
断言处理类,用于抛出各种API异常:
publicclassAsserts{publicstaticvoidfail(String message){thrownewApiException(message);}publicstaticvoidfail(IErrorCode errorCode){thrownewApiException(errorCode);}}
全局异常处理:
@ControllerAdvicepublicclassGlobalExceptionHandler{@ResponseBody@ExceptionHandler(value =ApiException.class)publicCommonResulthandle(ApiException e){if(e.getErrorCode()!=null){returnCommonResult.failed(e.getErrorCode());}returnCommonResult.failed(e.getMessage());}@ResponseBody@ExceptionHandler(value =MethodArgumentNotValidException.class)publicCommonResulthandleValidException(MethodArgumentNotValidException e){BindingResult bindingResult = e.getBindingResult();String message =null;if(bindingResult.hasErrors()){FieldError fieldError = bindingResult.getFieldError();if(fieldError !=null){
message = fieldError.getField()+fieldError.getDefaultMessage();}}returnCommonResult.validateFailed(message);}@ResponseBody@ExceptionHandler(value =BindException.class)publicCommonResulthandleValidException(BindException e){BindingResult bindingResult = e.getBindingResult();String message =null;if(bindingResult.hasErrors()){FieldError fieldError = bindingResult.getFieldError();if(fieldError !=null){
message = fieldError.getField()+fieldError.getDefaultMessage();}}returnCommonResult.validateFailed(message);}}
统一日志处理切面:
@Aspect@Component@Order(1)publicclassWebLogAspect{privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(WebLogAspect.class);@Pointcut("execution(public * com.*.*.controller.*.*(..))||execution(public * com.*.*.controller.*.*(..))")publicvoidwebLog(){}@Before("webLog()")publicvoiddoBefore(JoinPoint joinPoint)throwsThrowable{}@AfterReturning(value ="webLog()", returning ="ret")publicvoiddoAfterReturning(Object ret)throwsThrowable{}@Around("webLog()")publicObjectdoAround(ProceedingJoinPoint joinPoint)throwsThrowable{long startTime =System.currentTimeMillis();//获取当前请求对象ServletRequestAttributes attributes =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();//记录请求信息(通过Logstash传入Elasticsearch)WebLog webLog =newWebLog();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 =newHashMap<>();
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;}/**
* 根据方法和传入的参数获取请求参数
*/privateObjectgetParameter(Method method,Object[] args){List<Object> argList =newArrayList<>();Parameter[] parameters = method.getParameters();for(int i =0; i < parameters.length; i++){//将RequestBody注解修饰的参数作为请求参数RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);if(requestBody !=null){
argList.add(args[i]);}//将RequestParam注解修饰的参数作为请求参数RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);if(requestParam !=null){Map<String,Object> map =newHashMap<>();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){returnnull;}elseif(argList.size()==1){return argList.get(0);}else{return argList;}}}
redis操作Service:
publicinterfaceRedisService{/**
* 保存属性
*/voidset(String key,Object value,long time);/**
* 保存属性
*/voidset(String key,Object value);/**
* 获取属性
*/Objectget(String key);/**
* 删除属性
*/Booleandel(String key);/**
* 批量删除属性
*/Longdel(List<String> keys);/**
* 设置过期时间
*/Booleanexpire(String key,long time);/**
* 获取过期时间
*/LonggetExpire(String key);/**
* 判断是否有该属性
*/BooleanhasKey(String key);/**
* 按delta递增
*/Longincr(String key,long delta);/**
* 按delta递减
*/Longdecr(String key,long delta);/**
* 获取Hash结构中的属性
*/ObjecthGet(String key,String hashKey);/**
* 向Hash结构中放入一个属性
*/BooleanhSet(String key,String hashKey,Object value,long time);/**
* 向Hash结构中放入一个属性
*/voidhSet(String key,String hashKey,Object value);/**
* 直接获取整个Hash结构
*/Map<Object,Object>hGetAll(String key);/**
* 直接设置整个Hash结构
*/BooleanhSetAll(String key,Map<String,Object> map,long time);/**
* 直接设置整个Hash结构
*/voidhSetAll(String key,Map<String,?> map);/**
* 删除Hash结构中的属性
*/voidhDel(String key,Object... hashKey);/**
* 判断Hash结构中是否有该属性
*/BooleanhHasKey(String key,String hashKey);/**
* Hash结构中属性递增
*/LonghIncr(String key,String hashKey,Long delta);/**
* Hash结构中属性递减
*/LonghDecr(String key,String hashKey,Long delta);/**
* 获取Set结构
*/Set<Object>sMembers(String key);/**
* 向Set结构中添加属性
*/LongsAdd(String key,Object... values);/**
* 向Set结构中添加属性
*/LongsAdd(String key,long time,Object... values);/**
* 是否为Set中的属性
*/BooleansIsMember(String key,Object value);/**
* 获取Set结构的长度
*/LongsSize(String key);/**
* 删除Set结构中的属性
*/LongsRemove(String key,Object... values);/**
* 获取List结构中的属性
*/List<Object>lRange(String key,long start,long end);/**
* 获取List结构的长度
*/LonglSize(String key);/**
* 根据索引获取List中的属性
*/ObjectlIndex(String key,long index);/**
* 向List结构中添加属性
*/LonglPush(String key,Object value);/**
* 向List结构中添加属性
*/LonglPush(String key,Object value,long time);/**
* 向List结构中批量添加属性
*/LonglPushAll(String key,Object... values);/**
* 向List结构中批量添加属性
*/LonglPushAll(String key,Long time,Object... values);/**
* 从List结构中移除属性
*/LonglRemove(String key,long count,Object value);}
redis操作实现类:
publicclassRedisServiceImplimplementsRedisService{@AutowiredprivateRedisTemplate<String,Object> redisTemplate;@Overridepublicvoidset(String key,Object value,long time){
redisTemplate.opsForValue().set(key, value, time,TimeUnit.SECONDS);}@Overridepublicvoidset(String key,Object value){
redisTemplate.opsForValue().set(key, value);}@OverridepublicObjectget(String key){return redisTemplate.opsForValue().get(key);}@OverridepublicBooleandel(String key){return redisTemplate.delete(key);}@OverridepublicLongdel(List<String> keys){return redisTemplate.delete(keys);}@OverridepublicBooleanexpire(String key,long time){return redisTemplate.expire(key, time,TimeUnit.SECONDS);}@OverridepublicLonggetExpire(String key){return redisTemplate.getExpire(key,TimeUnit.SECONDS);}@OverridepublicBooleanhasKey(String key){return redisTemplate.hasKey(key);}@OverridepublicLongincr(String key,long delta){return redisTemplate.opsForValue().increment(key, delta);}@OverridepublicLongdecr(String key,long delta){return redisTemplate.opsForValue().increment(key,-delta);}@OverridepublicObjecthGet(String key,String hashKey){return redisTemplate.opsForHash().get(key, hashKey);}@OverridepublicBooleanhSet(String key,String hashKey,Object value,long time){
redisTemplate.opsForHash().put(key, hashKey, value);returnexpire(key, time);}@OverridepublicvoidhSet(String key,String hashKey,Object value){
redisTemplate.opsForHash().put(key, hashKey, value);}@OverridepublicMap<Object,Object>hGetAll(String key){return redisTemplate.opsForHash().entries(key);}@OverridepublicBooleanhSetAll(String key,Map<String,Object> map,long time){
redisTemplate.opsForHash().putAll(key, map);returnexpire(key, time);}@OverridepublicvoidhSetAll(String key,Map<String,?> map){
redisTemplate.opsForHash().putAll(key, map);}@OverridepublicvoidhDel(String key,Object... hashKey){
redisTemplate.opsForHash().delete(key, hashKey);}@OverridepublicBooleanhHasKey(String key,String hashKey){return redisTemplate.opsForHash().hasKey(key, hashKey);}@OverridepublicLonghIncr(String key,String hashKey,Long delta){return redisTemplate.opsForHash().increment(key, hashKey, delta);}@OverridepublicLonghDecr(String key,String hashKey,Long delta){return redisTemplate.opsForHash().increment(key, hashKey,-delta);}@OverridepublicSet<Object>sMembers(String key){return redisTemplate.opsForSet().members(key);}@OverridepublicLongsAdd(String key,Object... values){return redisTemplate.opsForSet().add(key, values);}@OverridepublicLongsAdd(String key,long time,Object... values){Long count = redisTemplate.opsForSet().add(key, values);expire(key, time);return count;}@OverridepublicBooleansIsMember(String key,Object value){return redisTemplate.opsForSet().isMember(key, value);}@OverridepublicLongsSize(String key){return redisTemplate.opsForSet().size(key);}@OverridepublicLongsRemove(String key,Object... values){return redisTemplate.opsForSet().remove(key, values);}@OverridepublicList<Object>lRange(String key,long start,long end){return redisTemplate.opsForList().range(key, start, end);}@OverridepublicLonglSize(String key){return redisTemplate.opsForList().size(key);}@OverridepublicObjectlIndex(String key,long index){return redisTemplate.opsForList().index(key, index);}@OverridepublicLonglPush(String key,Object value){return redisTemplate.opsForList().rightPush(key, value);}@OverridepublicLonglPush(String key,Object value,long time){Long index = redisTemplate.opsForList().rightPush(key, value);expire(key, time);return index;}@OverridepublicLonglPushAll(String key,Object... values){return redisTemplate.opsForList().rightPushAll(key, values);}@OverridepublicLonglPushAll(String key,Long time,Object... values){Long count = redisTemplate.opsForList().rightPushAll(key, values);expire(key, time);return count;}@OverridepublicLonglRemove(String key,long count,Object value){return redisTemplate.opsForList().remove(key, count, value);}}
logback-spring.xml配置:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEconfiguration><configuration><!--引用默认日志配置--><includeresource="org/springframework/boot/logging/logback/defaults.xml"/><!--使用默认的控制台日志输出实现--><includeresource="org/springframework/boot/logging/logback/console-appender.xml"/><!--应用名称--><springPropertyscope="context"name="APP_NAME"source="spring.application.name"defaultValue="springBoot"/><!--日志文件保存路径--><propertyname="LOG_FILE_PATH"value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/logs}"/><!--LogStash访问host--><springPropertyname="LOG_STASH_HOST"scope="context"source="logstash.host"defaultValue="localhost"/><!--DEBUG日志输出到文件--><appendername="FILE_DEBUG"class="ch.qos.logback.core.rolling.RollingFileAppender"><!--输出DEBUG以上级别日志--><filterclass="ch.qos.logback.classic.filter.ThresholdFilter"><level>DEBUG</level></filter><encoder><!--设置为默认的文件日志格式--><pattern>${FILE_LOG_PATTERN}</pattern><charset>UTF-8</charset></encoder><rollingPolicyclass="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!--设置文件命名格式--><fileNamePattern>${LOG_FILE_PATH}/debug/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern><!--设置日志文件大小,超过就重新生成文件,默认10M--><maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize><!--日志文件保留天数,默认30天--><maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory></rollingPolicy></appender><!--ERROR日志输出到文件--><appendername="FILE_ERROR"class="ch.qos.logback.core.rolling.RollingFileAppender"><!--只输出ERROR级别的日志--><filterclass="ch.qos.logback.classic.filter.LevelFilter"><level>ERROR</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter><encoder><!--设置为默认的文件日志格式--><pattern>${FILE_LOG_PATTERN}</pattern><charset>UTF-8</charset></encoder><rollingPolicyclass="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!--设置文件命名格式--><fileNamePattern>${LOG_FILE_PATH}/error/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern><!--设置日志文件大小,超过就重新生成文件,默认10M--><maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize><!--日志文件保留天数,默认30天--><maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory></rollingPolicy></appender><!--DEBUG日志输出到LogStash--><appendername="LOG_STASH_DEBUG"class="net.logstash.logback.appender.LogstashTcpSocketAppender"><filterclass="ch.qos.logback.classic.filter.ThresholdFilter"><level>DEBUG</level></filter><destination>${LOG_STASH_HOST}:4560</destination><encodercharset="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服务时,设置访问策略为轮询--><connectionStrategy><roundRobin><connectionTTL>5 minutes</connectionTTL></roundRobin></connectionStrategy></appender><!--ERROR日志输出到LogStash--><appendername="LOG_STASH_ERROR"class="net.logstash.logback.appender.LogstashTcpSocketAppender"><filterclass="ch.qos.logback.classic.filter.LevelFilter"><level>ERROR</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter><destination>${LOG_STASH_HOST}:4561</destination><encodercharset="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服务时,设置访问策略为轮询--><connectionStrategy><roundRobin><connectionTTL>5 minutes</connectionTTL></roundRobin></connectionStrategy></appender><!--业务日志输出到LogStash--><appendername="LOG_STASH_BUSINESS"class="net.logstash.logback.appender.LogstashTcpSocketAppender"><destination>${LOG_STASH_HOST}:4562</destination><encodercharset="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服务时,设置访问策略为轮询--><connectionStrategy><roundRobin><connectionTTL>5 minutes</connectionTTL></roundRobin></connectionStrategy></appender><!--接口访问记录日志输出到LogStash--><appendername="LOG_STASH_RECORD"class="net.logstash.logback.appender.LogstashTcpSocketAppender"><destination>${LOG_STASH_HOST}:4563</destination><encodercharset="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服务时,设置访问策略为轮询--><connectionStrategy><roundRobin><connectionTTL>5 minutes</connectionTTL></roundRobin></connectionStrategy></appender><!--控制框架输出日志--><loggername="org.slf4j"level="INFO"/><loggername="springfox"level="INFO"/><loggername="io.swagger"level="INFO"/><loggername="org.springframework"level="INFO"/><loggername="org.hibernate.validator"level="INFO"/><loggername="com.alibaba.nacos.client.naming"level="INFO"/><rootlevel="DEBUG"><appender-refref="CONSOLE"/><appender-refref="FILE_DEBUG"/><appender-refref="FILE_ERROR"/><appender-refref="LOG_STASH_DEBUG"/><appender-refref="LOG_STASH_ERROR"/></root><loggername="com.*.*.common.log.WebLogAspect"level="DEBUG"><appender-refref="LOG_STASH_RECORD"/></logger><loggername="com.*.*"level="DEBUG"><appender-refref="LOG_STASH_BUSINESS"/></logger></configuration>
2.admin代码部分
Swagger API文档相关配置:
@Configuration@EnableSwagger2publicclassSwaggerConfigextendsBaseSwaggerConfig{@OverridepublicSwaggerPropertiesswaggerProperties(){returnSwaggerProperties.builder().apiBasePackage("com.admin.controller").title("后台系统").description("后台相关接口文档").contactName("admin").version("1.0").enableSecurity(true).build();}@BeanpublicBeanPostProcessorspringfoxHandlerProviderBeanPostProcessor(){returngenerateBeanPostProcessor();}}
后台用户管理:
@Controller@Api(tags ="UmsAdminController", description ="后台用户管理")@RequestMapping("/admin")publicclassUmsAdminController{@AutowiredprivateUmsAdminService adminService;@ApiOperation(value ="用户注册")@RequestMapping(value ="/register", method =RequestMethod.POST)@ResponseBodypublicCommonResult<UmsAdmin>register(@Validated@RequestBodyUmsAdminParam umsAdminParam){UmsAdmin umsAdmin = adminService.register(umsAdminParam);if(umsAdmin ==null){returnCommonResult.failed();}returnCommonResult.success(umsAdmin);}@ApiOperation(value ="登录以后返回token")@RequestMapping(value ="/login", method =RequestMethod.POST)@ResponseBodypublicCommonResultlogin(@Validated@RequestBodyUmsAdminLoginParam umsAdminLoginParam){return adminService.login(umsAdminLoginParam.getUsername(),umsAdminLoginParam.getPassword());}@ApiOperation(value ="登出功能")@RequestMapping(value ="/logout", method =RequestMethod.POST)@ResponseBodypublicCommonResultlogout(){returnCommonResult.success(null);}@ApiOperation("根据用户名获取通用用户信息")@RequestMapping(value ="/loadByUsername", method =RequestMethod.GET)@ResponseBodypublicUserDtoloadUserByUsername(@RequestParamString username){UserDto userDTO = adminService.loadUserByUsername(username);return userDTO;}}
UmsAdminService实现类:
@ServicepublicclassUmsAdminServiceImplimplementsUmsAdminService{privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(UmsAdminServiceImpl.class);@AutowiredprivateUmsAdminMapper adminMapper;@AutowiredprivateUmsAdminLoginLogMapper loginLogMapper;@AutowiredprivateAuthService authService;@AutowiredprivateHttpServletRequest request;@OverridepublicUmsAdminregister(UmsAdminParam umsAdminParam){UmsAdmin umsAdmin =newUmsAdmin();BeanUtils.copyProperties(umsAdminParam, umsAdmin);
umsAdmin.setCreateTime(newDate());
umsAdmin.setStatus(1);//查询是否有相同用户名的用户List<UmsAdmin> umsAdminList = adminMapper.selectList(newQueryWrapper<UmsAdmin>().eq("username",umsAdmin.getUsername()));if(umsAdminList.size()>0){returnnull;}//将密码进行加密操作String encodePassword =BCrypt.hashpw(umsAdmin.getPassword());
umsAdmin.setPassword(encodePassword);
adminMapper.insert(umsAdmin);return umsAdmin;}@OverridepublicCommonResultlogin(String username,String password){if(StrUtil.isEmpty(username)||StrUtil.isEmpty(password)){Asserts.fail("用户名或密码不能为空!");}Map<String,String> params =newHashMap<>();
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;}@OverridepublicUserDtoloadUserByUsername(String username){//获取用户信息UmsAdmin admin =getAdminByUsername(username);if(admin !=null){UserDto userDTO =newUserDto();BeanUtils.copyProperties(admin,userDTO);return userDTO;}returnnull;}@OverridepublicUmsAdmingetAdminByUsername(String username){List<UmsAdmin> adminList = adminMapper.selectList(newQueryWrapper<UmsAdmin>().eq("username",username));if(adminList !=null&& adminList.size()>0){return adminList.get(0);}returnnull;}}
认证服务远程调用Service:
@FeignClient("oauth")publicinterfaceAuthService{@PostMapping(value ="/oauth/token")CommonResultgetAccessToken(@RequestParamMap<String,String> parameters);}
后台管理员Service:
publicinterfaceUmsAdminService{/**
* 注册功能
*/UmsAdminregister(UmsAdminParam umsAdminParam);/**
* 登录功能
* @param username 用户名
* @param password 密码
* @return 调用认证中心返回结果
*/CommonResultlogin(String username,String password);/**
* 获取用户信息
*/UserDtoloadUserByUsername(String username);/**
* 根据用户名获取后台管理员
*/UmsAdmingetAdminByUsername(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 #最小空闲连接数
max-active:20 #最大连接数
web-stat-filter:
exclusions:"*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" #不统计这些请求数据
stat-view-servlet: #访问监控网页的登录用户名和密码
login-username: druid
login-password: druid
redis:
host: localhost # Redis服务器地址
database:0 # Redis数据库索引(默认为0)
port:6379 # Redis服务器连接端口
password: # Redis服务器连接密码(默认为空)
timeout:3000ms # 连接超时时间(毫秒)
management: #开启SpringBootAdmin的监控
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
总结
本人第一次写文章,如有看不懂的多多包涵
以上就是今天要讲的内容,本文仅仅简单介绍了统一认证的使用,后续会把源码地址分享出来。
项目地址:https://gitee.com/zhouwudi/SpringSecurityOauth2
版权归原作者 Smiling-boy 所有, 如有侵权,请联系我们删除。