Spring Clou整合 Security + Oauth2 + jwt实现权限认证的详细过程
作者:Smiling-boy
前言
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代码部分
代码如下(示例):
配置鉴权管理器
@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)); } //管理端路径需校验权限 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失效时的返回结果:
@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)); } }
无权限访问的返回结果:
@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)); } }
全局跨域配置:注意:前端从网关进行调用时需要配置
@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); } }
网关白名单配置:
@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请求头过期或签名错误的结果 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(); } @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->根据路径拼接成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的各个配置节点:
@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设置 */ @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转化成用户信息的全局过滤器:
@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中解析用户信息并设置到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数据库索引(默认为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
3.oauth2代码部分
JWT内容增强器:
@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设置到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的内容增强器 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相关配置:
@Configuration @EnableSwagger2 public class SwaggerConfig extends BaseSwaggerConfig { @Override public SwaggerProperties swaggerProperties() { return SwaggerProperties.builder() .apiBasePackage("com.oauth.oauth2.controller") .title("认证中心") .description("认证中心相关接口文档") .contactName("oauth") .version("1.0") .enableSecurity(true) .build(); } @Bean public BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { return generateBeanPostProcessor(); } }
SpringSecurity相关配置:
@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 = "该账户已被禁用,请联系管理员!"; public static final String ACCOUNT_LOCKED = "该账号已被锁定,请联系管理员!"; public static final String ACCOUNT_EXPIRED = "该账号已过期,请联系管理员!"; public static final String PERMISSION_DENIED = "没有访问权限,请联系管理员!"; }
自定义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("授权模式") @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; /** * 用户状态 */ private Boolean enabled; /** * 登录客户端ID */ private String clientId; /** * 权限数据 */ 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()); } }
后台用户服务远程调用Service:
@FeignClient("admin") public interface UmsAdminService { @GetMapping("/admin/loadByUsername") UserDto loadUserByUsername(@RequestParam String username); }
用户管理业务类:
@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; } /** * 成功返回结果 * * @param data 获取的数据 */ public static <T> CommonResult<T> success(T data) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); } /** * 成功返回结果 * * @param data 获取的数据 * @param message 提示信息 */ public static <T> CommonResult<T> success(T data, String message) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data); } /** * 失败返回结果 * @param errorCode 错误码 */ public static <T> CommonResult<T> failed(IErrorCode errorCode) { return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null); } /** * 失败返回结果 * @param errorCode 错误码 * @param message 错误信息 */ public static <T> CommonResult<T> failed(IErrorCode errorCode,String message) { return new CommonResult<T>(errorCode.getCode(), message, null); } /** * 失败返回结果 * @param message 提示信息 */ public static <T> CommonResult<T> failed(String message) { return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null); } /** * 失败返回结果 */ public static <T> CommonResult<T> failed() { return failed(ResultCode.FAILED); } /** * 参数验证失败返回结果 */ public static <T> CommonResult<T> validateFailed() { return failed(ResultCode.VALIDATE_FAILED); } /** * 参数验证失败返回结果 * @param message 提示信息 */ public static <T> CommonResult<T> validateFailed(String message) { return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null); } /** * 未登录返回结果 */ public static <T> CommonResult<T> unauthorized(T data) { return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); } /** * 未授权返回结果 */ 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, "参数检验失败"), UNAUTHORIZED(401, "暂未登录或token已经过期"), FORBIDDEN(403, "没有相关权限"); 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基础配置:
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() { //创建JSON序列化器 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); //必须设置,否则无法将JSON转化为对象,会转化成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); //设置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基础配置:
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() { //设置请求头信息 List<SecurityScheme> result = new ArrayList<>(); ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header"); result.add(apiKey); return result; } private List<SecurityContext> securityContexts() { //设置需要登录认证的路径 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(); }
权限相关常量定义:
public interface AuthConstant { /** * 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) @Builder public class SwaggerProperties { /** * API文档生成基础路径 */ private String apiBasePackage; /** * 是否要启用登录认证 */ private boolean enableSecurity; /** * 文档标题 */ private String title; /** * 文档描述 */ private String description; /** * 文档版本 */ private String version; /** * 文档联系人姓名 */ private String contactName; /** * 文档联系人网址 */ private String contactUrl; /** * 文档联系人邮箱 */ 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; /** * 请求参数 */ private Object parameter; /** * 返回结果 */ 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); } }
统一日志处理切面:
@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; } /** * 根据方法和传入的参数获取请求参数 */ 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注解修饰的参数作为请求参数 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 = 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); /** * 设置过期时间 */ 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结构中的属性 */ Object hGet(String key, String hashKey); /** * 向Hash结构中放入一个属性 */ Boolean hSet(String key, String hashKey, Object value, long time); /** * 向Hash结构中放入一个属性 */ void hSet(String key, String hashKey, Object value); /** * 直接获取整个Hash结构 */ Map<Object, Object> hGetAll(String key); /** * 直接设置整个Hash结构 */ Boolean hSetAll(String key, Map<String, Object> map, long time); /** * 直接设置整个Hash结构 */ void hSetAll(String key, Map<String, ?> map); /** * 删除Hash结构中的属性 */ void hDel(String key, Object... hashKey); /** * 判断Hash结构中是否有该属性 */ Boolean hHasKey(String key, String hashKey); /** * Hash结构中属性递增 */ Long hIncr(String key, String hashKey, Long delta); /** * Hash结构中属性递减 */ Long hDecr(String key, String hashKey, Long delta); /** * 获取Set结构 */ Set<Object> sMembers(String key); /** * 向Set结构中添加属性 */ Long sAdd(String key, Object... values); /** * 向Set结构中添加属性 */ Long sAdd(String key, long time, Object... values); /** * 是否为Set中的属性 */ Boolean sIsMember(String key, Object value); /** * 获取Set结构的长度 */ Long sSize(String key); /** * 删除Set结构中的属性 */ Long sRemove(String key, Object... values); /** * 获取List结构中的属性 */ List<Object> lRange(String key, long start, long end); /** * 获取List结构的长度 */ Long lSize(String key); /** * 根据索引获取List中的属性 */ Object lIndex(String key, long index); /** * 向List结构中添加属性 */ Long lPush(String key, Object value); /** * 向List结构中添加属性 */ Long lPush(String key, Object value, long time); /** * 向List结构中批量添加属性 */ Long lPushAll(String key, Object... values); /** * 向List结构中批量添加属性 */ Long lPushAll(String key, Long time, Object... values); /** * 从List结构中移除属性 */ Long lRemove(String key, long count, Object value); }
redis操作实现类:
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"/> <!--使用默认的控制台日志输出实现--> <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> <!--设置为默认的文件日志格式--> <pattern>${FILE_LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="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日志输出到文件--> <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> <!--设置为默认的文件日志格式--> <pattern>${FILE_LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="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--> <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服务时,设置访问策略为轮询--> <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服务时,设置访问策略为轮询--> <connectionStrategy> <roundRobin> <connectionTTL>5 minutes</connectionTTL> </roundRobin> </connectionStrategy> </appender> <!--业务日志输出到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服务时,设置访问策略为轮询--> <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服务时,设置访问策略为轮询--> <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文档相关配置:
@Configuration @EnableSwagger2 public class SwaggerConfig extends BaseSwaggerConfig { @Override public SwaggerProperties swaggerProperties() { return SwaggerProperties.builder() .apiBasePackage("com.admin.controller") .title("后台系统") .description("后台相关接口文档") .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("根据用户名获取通用用户信息") @RequestMapping(value = "/loadByUsername", method = RequestMethod.GET) @ResponseBody public UserDto loadUserByUsername(@RequestParam String username) { UserDto userDTO = adminService.loadUserByUsername(username); return userDTO; } }
UmsAdminService实现类:
@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; } }
认证服务远程调用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 调用认证中心返回结果 */ CommonResult login(String username, String password); /** * 获取用户信息 */ UserDto loadUserByUsername(String username); /** * 根据用户名获取后台管理员 */ 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 #最小空闲连接数 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: #开启SpringBoot Admin的监控 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
到此这篇关于Spring Cloud 完整整合 Security + Oauth2 + jwt实现权限认证的文章就介绍到这了,更多相关Spring Cloud Security + Oauth2 + jwt权限认证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!