赞
踩
Oauth2.0简单解释
Oauth2.0四种方式
什么是JWT
JWT无状态登录
Spring security 系列15篇
Spring boot security 学习
Spring Security Oauth2 permitAll()还校验token
源码分析
UsernamePasswordAuthenticationFilter
网上oauth2相关的demo讲的很笼统,几乎都是内存配置的方式简单演示了一下。
这段时间踩了很多坑,因此整理出了这篇文章
本文解决了如下问题:
未解决的问题:
1. 使用nginx做了负载均衡,搭建了两台资源服务器和两台认证服务器A B,登录资源服务器登录时调用了认证服务器A,获取授权码时(/oauth/authorize)调用了认证服务器B,返回401,尚不知道如何解决?
客户使用多个系统,每个系统都有自己的账号密码,想通过我们的门户系统进行登录授权(其他系统登需要登录时,跳转门户进行登录授权)
前端服务器
资源服务器(子系统)
认证服务器
认证服务器和资源服务器分为两个项目(模块)
前端与资源服务器交互(是否登录过):通过session交互
前端与资源服务器交互(每次都会调用认证服务器的/check_token):通过token(header中的Authorization属性)交互
最初使用spring boot oauth2依赖有问题(据说不再维护)
因此采用spring-cloud-starter-oauth2的依赖
文章只提供oauth2 jwt相关的依赖
<properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR9</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies>
表含义这里不多做介绍了,都是security oauth2框架使用数据库模式需要的表,认证配置的表是:oauth_client_details ,需要在该表中配置客户端id,回调URL等信息
CREATE TABLE oauth_access_token ( token_id VARCHAR(256) NULL DEFAULT NULL, token TEXT NULL DEFAULT NULL, authentication_id VARCHAR(128) NOT NULL PRIMARY KEY, user_name VARCHAR(256) NULL DEFAULT NULL, client_id VARCHAR(256) NULL DEFAULT NULL, authentication text NULL DEFAULT NULL, refresh_token VARCHAR(256) NULL DEFAULT NULL); ALTER TABLE public.oauth_access_token OWNER to dna_portal; CREATE TABLE oauth_approvals ( userId VARCHAR(256) NULL DEFAULT NULL, clientId VARCHAR(256) NULL DEFAULT NULL, scope VARCHAR(256) NULL DEFAULT NULL, status VARCHAR(10) NULL DEFAULT NULL, expiresAt time NULL DEFAULT NULL, lastModifiedAt time NULL DEFAULT NULL); ALTER TABLE public.oauth_approvals OWNER to dna_portal; CREATE TABLE oauth_client_details ( client_id VARCHAR(128) NOT NULL PRIMARY KEY, resource_ids VARCHAR(256) NULL DEFAULT NULL, client_secret VARCHAR(256) NULL DEFAULT NULL, scope VARCHAR(256) NULL DEFAULT NULL, authorized_grant_types VARCHAR(256) NULL DEFAULT NULL, web_server_redirect_uri VARCHAR(256) NULL DEFAULT NULL, authorities VARCHAR(256) NULL DEFAULT NULL, access_token_validity INT8 NULL DEFAULT NULL, refresh_token_validity INT8 NULL DEFAULT NULL, additional_information VARCHAR(4096) NULL DEFAULT NULL, autoapprove VARCHAR(256) NULL DEFAULT NULL); ALTER TABLE public.oauth_client_details OWNER to dna_portal; insert into oauth_client_details (client_id,resource_ids,client_secret,scope,authorized_grant_types, web_server_redirect_uri,authorities,access_token_validity,refresh_token_validity,additional_information,autoapprove) values ('website', 'website', '$2a$10$.ebjcgCVOHuEscJ6xLyQcu21nW93XuHZ2qk2TRbTofDLVhPY0C5S2', 'all', 'authorization_code,refresh_token', 'http://127.0.0.1:8080/web-portal/su/token', 'admin,ROLE_admin',36000, null, null, true); CREATE TABLE oauth_client_token ( token_id VARCHAR(256) NULL DEFAULT NULL, token text NULL DEFAULT NULL, authentication_id VARCHAR(128) NOT NULL PRIMARY KEY, user_name VARCHAR(256) NULL DEFAULT NULL, client_id VARCHAR(256) NULL DEFAULT NULL); ALTER TABLE public.oauth_client_token OWNER to dna_portal; CREATE TABLE oauth_code ( code VARCHAR(256) NULL DEFAULT NULL, authentication text NULL DEFAULT NULL); ALTER TABLE public.oauth_code OWNER to dna_portal; CREATE TABLE oauth_refresh_token ( token_id VARCHAR(256) NULL DEFAULT NULL, token text NULL DEFAULT NULL, authentication text NULL DEFAULT NULL); ALTER TABLE public.oauth_refresh_token OWNER to dna_portal;
跨域配置
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { /** * 跨域访问配置 */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600) .allowedHeaders("*"); } }
资源服务器配置
# 本应用占用端口 server: port: 8008 servlet: context-path: /web-portal session: cookie: name: OAUTH2-PORTAL-SESSIONID oauth2: server: # oauth_client_details配置的client_id clientId: website # oauth_client_details配置的client_secret(明文) clientSecret: 2020 # oauth2-server服务的地址 tokenAddr: http://127.0.0.1:8080/oauth2-server/oauth/token checkTokenAddr: http://127.0.0.1:8080/oauth2-server/oauth/check_token # jwt密钥 jwtSecret: drinkless-jwt-key
import com.drinkless.portal.filter.JWTAuthenticationFilter; import com.drinkless.portal.handlder.ApiAccessDeniedHandler; import com.drinkless.portal.handlder.AuthExceptionEntryPoint; import com.drinkless.portal.handlder.CustomerResponseErrorHandler; import com.drinkless.portal.handlder.LoginExpireHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.RemoteTokenServices; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.web.client.RestTemplate; @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Value("${oauth2.server.checkTokenAddr}") private String checkTokenAddr; @Value("${oauth2.server.clientId}") private String clientId; @Value("${oauth2.server.clientSecret}") private String clientSecret; @Value("${oauth2.server.jwtSecret}") private String jwtSecret; //放行接口 swagger,系统连接性检查 和一些不需要登录就可访问的接口 public static String[] passUrl = {"/su/token/**", "/region/getTree", "/app/status", "/sd/getDictList/**", "/version", "/article/getList", "/article/find/**", "/advertisement/list", "/category/getCategoryList", "/category/find/**", "/advertisement/list", "/_health", "/v2/api-docs", "/swagger-resources/configuration/ui","/swagger-resources", "/swagger-resources/configuration/security", "/swagger-ui.html","/css/**", "/js/**","/images/**", "/webjars/**", "**/favicon.ico"}; @Bean @LoadBalanced public RestTemplate restTemplate(RestTemplateBuilder builder){ RestTemplate restTemplate = builder.build(); /*为RestTemplate配置异常处理器0*/ restTemplate.setErrorHandler(new CustomerResponseErrorHandler()); return restTemplate; } //不使用权限校验的ROLE_前缀 (http.servletApi().rolePrefix("");该方式无效果,使用@Bean方式) @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } @Autowired private RestTemplate restTemplate; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId(clientId); resources.tokenStore(new JwtTokenStore(accessTokenConverter())).stateless(true); /* 配置令牌验证 */ RemoteTokenServices remoteTokenServices = new RemoteTokenServices(); remoteTokenServices.setAccessTokenConverter(accessTokenConverter()); remoteTokenServices.setRestTemplate(restTemplate); remoteTokenServices.setCheckTokenEndpointUrl(checkTokenAddr); remoteTokenServices.setClientId(clientId); remoteTokenServices.setClientSecret(clientSecret); resources.tokenServices(remoteTokenServices).stateless(true); //check_token异常类 resources.authenticationEntryPoint(new AuthExceptionEntryPoint()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(jwtSecret); return converter; } /* 配置资源拦截规则 */ @Override public void configure(HttpSecurity http) throws Exception { //关闭csrf http.cors().and().csrf().disable(); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); //放行接口配置 http.authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() // 对option不校验 .antMatchers(passUrl).permitAll() .anyRequest().authenticated(); //jwt校验 http.addFilterBefore(new JWTAuthenticationFilter(), AbstractPreAuthenticatedProcessingFilter.class); //登录超时未登录处理 http.exceptionHandling().authenticationEntryPoint(new LoginExpireHandler()); //权限不足处理器 配合注解使用@Secured("admin") http.exceptionHandling().accessDeniedHandler(new ApiAccessDeniedHandler()); } }
JWT过滤器
package com.drinkless.portal.filter; import com.fasterxml.jackson.databind.ObjectMapper; import com.drinkless.portal.common.constants.RedisConstant; import com.drinkless.portal.common.entity.vo.SysUserVo; import com.drinkless.portal.common.utils.ResultUtil; import com.drinkless.portal.common.utils.SysUserUtil; import com.drinkless.portal.config.ResourceServerConfig; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.context.ApplicationContext; import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.context.support.WebApplicationContextUtils; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; /** * jwt过滤器 配合redis校验 */ @Slf4j public class JWTAuthenticationFilter implements Filter { private final ObjectMapper mapper = new ObjectMapper(); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { log.info("【JWT过滤器】start"); HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; //如果是放行接口,构造出无token的request后直接放行(这样做的话不会调用/oauth/check_token接口进行校验) String uri = request.getRequestURI(); log.info("uri_{}:{}, request.getContentType(): {}", request.getMethod(), uri, request.getContentType()); if (!isNeedFilter(uri, ResourceServerConfig.passUrl)) { request = new HttpServletRequestWrapper(request) { private Set<String> headerNameSet; @Override public Enumeration<String> getHeaderNames() { if (headerNameSet == null) { // first time this method is called, cache the wrapped request's header names: headerNameSet = new HashSet<>(); Enumeration<String> wrappedHeaderNames = super.getHeaderNames(); while (wrappedHeaderNames.hasMoreElements()) { String headerName = wrappedHeaderNames.nextElement(); if (!"Authorization".equalsIgnoreCase(headerName)) { headerNameSet.add(headerName); } } } return Collections.enumeration(headerNameSet); } @Override public Enumeration<String> getHeaders(String name) { if ("Authorization".equalsIgnoreCase(name)) { return Collections.<String>emptyEnumeration(); } return super.getHeaders(name); } @Override public String getHeader(String name) { if ("Authorization".equalsIgnoreCase(name)) { return null; } return super.getHeader(name); } }; filterChain.doFilter(request, response); } else { //非放行的接口 String header = request.getHeader(HttpHeaders.AUTHORIZATION); //是否过期 boolean expireFlag = false; try { if (StringUtils.isNotBlank(header)) { //token串 String token = header.substring(header.lastIndexOf("bearer") + 8); // 获取ValueOperations bean ServletContext context = request.getServletContext(); ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(context); ValueOperations valueOperations = (ValueOperations) ctx.getBean("valueOperations"); SysUserUtil sysUserUtil = (SysUserUtil) ctx.getBean("sysUserUtil"); SysUserVo user = sysUserUtil.getSysUserVoByToken(token); String redisToken = (String) valueOperations.get(RedisConstant.REDIS_LOGIN + user.getUsername()); // token失效 或 token不正确 if (StringUtils.isBlank(redisToken) || !token.equals(redisToken)) { expireFlag = true; } } else { // 没有token expireFlag = true; } } catch (Exception e) { expireFlag = true; log.error("【jwtToken】校验出错,异常信息:{}", e); } if (expireFlag) { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setCharacterEncoding("UTF-8"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write(this.mapper.writeValueAsString(ResultUtil.errorResult( HttpStatus.UNAUTHORIZED.value(), "token失效"))); } else { filterChain.doFilter(request, response); } } } /** * @param uri * @Description: 是否需要过滤 */ public boolean isNeedFilter(String uri, String[] includeUrls) { boolean isNeedFilter = true; for (String includeUrl : includeUrls) { includeUrl = includeUrl.replace("/**",""); if (includeUrl.equals(uri) || uri.contains(includeUrl)) { isNeedFilter = false; break; } } return isNeedFilter; } }
权限不足处理器
package com.drinkless.portal.handlder; import com.fasterxml.jackson.databind.ObjectMapper; import com.drinkless.portal.common.enums.HttpStatusEnum; import com.drinkless.portal.common.result.Result; import com.drinkless.portal.common.utils.ResultUtil; import org.springframework.http.MediaType; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * 403 权限不足处理器 */ public class ApiAccessDeniedHandler implements AccessDeniedHandler { public final ObjectMapper MAPPER = new ObjectMapper(); @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setStatus(HttpStatusEnum.FORBIDDEN.getCode()); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); Result result = ResultUtil.errorResult(HttpStatusEnum.FORBIDDEN.getCode(), "不允许访问"); PrintWriter out = response.getWriter(); out.write(MAPPER.writeValueAsString(result)); out.flush(); out.close(); } }
check_token异常类
package com.drinkless.portal.handlder; import com.fasterxml.jackson.databind.ObjectMapper; import com.drinkless.portal.common.enums.HttpStatusEnum; import com.drinkless.portal.common.result.Result; import com.drinkless.portal.common.utils.ResultUtil; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * check_token异常类 */ @Component public class AuthExceptionEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws ServletException { Map<String, Object> map = new HashMap<String, Object>(); Throwable cause = authException.getCause(); response.setStatus(HttpStatusEnum.UNAUTHORIZED.getCode()); response.setHeader("Content-Type", "application/json;charset=UTF-8"); try { if (cause instanceof InvalidTokenException) { Result result = ResultUtil.errorResult(HttpStatusEnum.UNAUTHORIZED.getCode(), "token失效"); response.getWriter().write(new ObjectMapper().writeValueAsString(result)); } } catch (IOException e) { e.printStackTrace(); } } }
restTemplate异常处理
package com.drinkless.portal.handlder; import org.springframework.http.client.ClientHttpResponse; import org.springframework.web.client.ResponseErrorHandler; import java.io.IOException; /** * restTemplate调用check_toke接口,token错误或失效,回返回400,抛出异常 * 我们不想要抛异常,想让我们自定义的AuthExceptionEntryPoint去处理 * 所以重写了hasError方法 */ public class CustomerResponseErrorHandler implements ResponseErrorHandler { @Override public boolean hasError(ClientHttpResponse response) throws IOException { // 这里返回false return false; } @Override public void handleError(ClientHttpResponse response) throws IOException { } }
登录超时 未登录处理器
package com.drinkless.portal.handlder; import com.fasterxml.jackson.databind.ObjectMapper; import com.drinkless.portal.common.enums.HttpStatusEnum; import com.drinkless.portal.common.result.Result; import com.drinkless.portal.common.utils.ResultUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Description: 登录超时 未登录处理器 * @Date: 2020/12/09 13:41 */ @Slf4j public class LoginExpireHandler implements AuthenticationEntryPoint { public final ObjectMapper MAPPER = new ObjectMapper(); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setStatus(HttpStatusEnum.UNAUTHORIZED.getCode()); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); Result result = ResultUtil.errorResult(HttpStatusEnum.UNAUTHORIZED.getCode(), "登录过期或未登录"); response.getWriter().write(MAPPER.writeValueAsString(result)); } }
controller
package com.drinkless.portal.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.drinkless.portal.common.constants.RedisConstant; import com.drinkless.portal.common.entity.dto.TokenDTO; import com.drinkless.portal.common.entity.dto.UpdatePasswordDTO; import com.drinkless.portal.common.entity.vo.SysUserVo; import com.drinkless.portal.common.enums.ExceptionEnum; import com.drinkless.portal.common.enums.ResultEnum; import com.drinkless.portal.common.result.Result; import com.drinkless.portal.common.service.SysUserService; import com.drinkless.portal.common.utils.ResultUtil; import com.drinkless.portal.common.utils.SysUserUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.*; import org.springframework.security.access.annotation.Secured; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import javax.validation.Valid; import java.io.UnsupportedEncodingException; import java.net.URI; import java.util.Arrays; import java.util.concurrent.TimeUnit; /** * @Description: 用户接口 * @Date: 2019/11/27 14:44 */ @Slf4j @RestController @RequestMapping("su") @Api(description = "系统用户相关接口") public class SysUserController { @Autowired private ValueOperations valueOperations; @Autowired private RestTemplate restTemplate; @Autowired private SysUserService sysUserService; @Autowired private SysUserUtil sysUserUtil; @Value("${oauth2.server.tokenAddr}") private String tokenAddr; @Value("${oauth2.server.clientId}") private String clientId; @Value("${oauth2.server.clientSecret}") private String clientSecret; // mapper 声明 private final ObjectMapper mapper = new ObjectMapper(); @GetMapping("token") @ApiOperation("获取token") public Result submitLogin(String code) throws Exception { RequestEntity httpEntity = new RequestEntity<>(getHttpBody(code), getHttpHeaders(), HttpMethod.POST, URI.create(tokenAddr)); ResponseEntity<TokenDTO> exchange = restTemplate.exchange(httpEntity, TokenDTO.class); if (exchange.getStatusCode().is2xxSuccessful()) { // redis管理jwtToken失效 TokenDTO tokenDTO = exchange.getBody(); String accessToken = tokenDTO.getAccessToken(); SysUserVo user = sysUserUtil.getSysUserVoByToken(accessToken); String expiresIn = tokenDTO.getExpiresIn(); valueOperations.set(RedisConstant.REDIS_LOGIN + user.getUsername(), accessToken, Long.parseLong(expiresIn), TimeUnit.SECONDS); return ResultUtil.successResult(ResultEnum.SUCCESS_STATUS, exchange.getBody()); } throw new RuntimeException("请求令牌失败!"); } private MultiValueMap<String, String> getHttpBody(String code) throws UnsupportedEncodingException { MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.add("code", code); params.add("grant_type", "authorization_code"); String redirectUri = sysUserService.selectOauthRedirectUri(clientId); params.add("redirect_uri", redirectUri); params.add("scope", "all"); return params; } private HttpHeaders getHttpHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setBasicAuth(clientId, clientSecret); httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); httpHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); return httpHeaders; } //密码重置功能只允许管理员调用 //sprngboot启动类还需要添加以下配置,才可生效 //@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) @GetMapping("password/reset/{id}") @ApiOperation("密码重置") @Secured("admin") public Result resetPassword(@PathVariable("id") Long id) { try { sysUserService.resetPassword(id); return ResultUtil.successResult(ResultEnum.SUCCESS_STATUS); } catch (Exception e) { log.error("【用户管理】密码重置异常, 异常信息:{}", e); return ResultUtil.errorResult(ExceptionEnum.UNKNOWN_EXCEPTION); } } @GetMapping("userInfo") @ApiOperation("获取用户信息接口") public Result<SysUserVo> queryUserInfo() { try { SysUserVo vo = null; String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (StringUtils.isNotBlank(header)) { //token串 String token = header.substring(header.lastIndexOf("bearer") + 8); String tokenBody = JwtUtils.testJwt(token); //token串转对象 JSONObject user = JSON.parseObject(tokenBody).getJSONObject("user"); vo = JSON.toJavaObject(user, SysUserVo.class); } return ResultUtil.successResult(ResultEnum.SUCCESS_STATUS, vo); } catch (Exception e) { log.error("【用户管理】查询用户信息异常, 异常信息:{}", e); return ResultUtil.errorResult(ExceptionEnum.UNKNOWN_EXCEPTION); } } }
token对象
/** * 授权成功token返回体 */ @Data public class TokenDTO { @JsonProperty("access_token") private String accessToken; @JsonProperty("refresh_token") private String refreshToken; @JsonProperty("token_type") private String tokenType; @JsonProperty("expires_in") private String expiresIn; @JsonProperty("scope") private String scope; }
jwt工具类
package com.drinkless.oauth2.server.utils; import org.springframework.stereotype.Component; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; @Component public class JwtUtils { /* 默认head */ public static final String DEFAULT_HEADER = "{\"alg\": \"HS256\",\"typ\": \"JWT\"}"; /* token有效时间 1天 */ public static final long EXPIRE_TIME = 1000*60*60*24; /* token在header中的名字 */ public static final String HEADER_TOKEN_NAME = "Authorization"; /** Base64URL 编码 */ public static String encode(String input) { return Base64.getUrlEncoder().encodeToString(input.getBytes()); } /** Base64URL 解码 */ public static String decode(String input) { return new String(Base64.getUrlDecoder().decode(input)); } /** * HmacSHA256 加密算法 * @param data 要加密的数据 * @param secret 秘钥 */ public static String HMACSHA256(String data, String secret) throws Exception { Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"); sha256_HMAC.init(secret_key); byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte item : array) { sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3); } return sb.toString().toUpperCase(); } /** 获取签名 */ public static String getSignature(String payload, String secret) throws Exception { return HMACSHA256(encode(DEFAULT_HEADER)+"."+encode(payload),secret); } /** * 验证jwt,正确返回载体数据,错误返回null * @param jwt */ public static String testJwt(String jwt) throws Exception { String[] jwts = jwt.split("\\."); return decode(jwts[1]); } }
资源服务器的配置到这里结束
server:
port: 8068
servlet:
context-path: /oauth2-server
oauth2:
server:
# jwt密钥
jwtSecret: drinkless-jwt-key
授权服务配置
package com.drinkless.oauth2.server.config; import com.drinkless.oauth2.server.service.UserDetailsServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import javax.sql.DataSource; import java.util.ArrayList; import java.util.List; /** * 授权服务配置 */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired @Qualifier("jwtTokenStore") private TokenStore tokenStore; @Autowired private DataSource dataSource; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired private JwtTokenEnhancer jwtTokenEnhancer; @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private PasswordEncoder passwordEncoder; /** * 配置一个客户端 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource).passwordEncoder(passwordEncoder); } /** 配置token管理 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain chain = new TokenEnhancerChain(); List<TokenEnhancer> list = new ArrayList<>(); list.add(jwtTokenEnhancer); list.add(jwtAccessTokenConverter); chain.setTokenEnhancers(list); //通过注入密码授权被打开AuthenticationManager endpoints.authenticationManager(authenticationManager) .tokenStore(tokenStore) //刷新令牌授权将包含对用户详细信息的检查,以确保该帐户仍然活动,因此需要配置userDetailsService .userDetailsService(userDetailsService) //配置令牌生成 .accessTokenConverter(jwtAccessTokenConverter) .tokenEnhancer(chain); // //该字段设置设置refresh token是否重复使用,true:reuse;false:no reuse. // .reuseRefreshTokens(true) } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.tokenKeyAccess("permitAll()") //允许所有人请求令牌 .checkTokenAccess("isAuthenticated()") //已验证的客户端才能请求check_token端点 .allowFormAuthenticationForClients(); // 单点登录配置 // security.tokenKeyAccess("isAuthenticated()"); } }
安全框架配置类
package com.drinkless.oauth2.server.config; import com.drinkless.oauth2.server.handlder.LoginExpireHandler; import com.drinkless.oauth2.server.handlder.LoginFailureHandler; import com.drinkless.oauth2.server.handlder.LoginSuccessHandler; import com.drinkless.oauth2.server.handlder.LogoutSuccessHandler; import com.drinkless.oauth2.server.service.UserDetailsServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import javax.sql.DataSource; /** * @Description: 安全框架配置类 * @Date: 2020/11/30 21:03 */ @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired @Qualifier("jwtTokenStore") private TokenStore tokenStore; @Override protected void configure(HttpSecurity http) throws Exception { //关闭csrf http.cors().and().csrf().disable(); // 授权配置 /* 开启授权认证 */ http.authorizeRequests() .antMatchers("/oauth/**").permitAll() //允许访问授权接口 .anyRequest().authenticated(); //登录页面 http.formLogin().loginProcessingUrl("/login"); // 登录成功或失败处理 http.formLogin().successHandler(new LoginSuccessHandler()) .failureHandler(new LoginFailureHandler()); // 登出授权及处理 http.logout().logoutUrl("/logout") .logoutSuccessHandler(new LogoutSuccessHandler(tokenStore)).permitAll(); //登录超时 未登录 http.exceptionHandling().authenticationEntryPoint(new LoginExpireHandler()); } @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); // 自动建表 //jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; } /** 授权服务配置需要用到这个bean */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** 加密算法 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } }
jwt token配置
package com.drinkless.oauth2.server.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import java.io.IOException; /** * jwt token配置 */ @Configuration public class JwtTokenStoreConfig { @Value("${oauth2.server.jwtSecret}") public String jwtSecret; @Bean public TokenStore jwtTokenStore()throws IOException { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() throws IOException { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //配置JWT使用的秘钥 accessTokenConverter.setSigningKey(jwtSecret); return accessTokenConverter; } @Bean public JwtTokenEnhancer jwtTokenEnhancer() { return new JwtTokenEnhancer(); } }
jwt token 定制
package com.drinkless.oauth2.server.config; import com.drinkless.oauth2.server.entity.SysUser; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import java.util.HashMap; import java.util.Map; /** * jwt token 定制 */ public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { SysUser sysUser = (SysUser) oAuth2Authentication.getPrincipal(); Map<String, Object> map = new HashMap<>(); sysUser.setPassword(null); map.put("user", sysUser); ((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map); return oAuth2AccessToken; } }
userDetailsService实现类(框架会根据返回用户信息中的密码校验和前端(request中的password)传的是否一致)
package com.drinkless.oauth2.server.service; import com.drinkless.oauth2.server.entity.SysUser; import com.drinkless.oauth2.server.exception.MyUsernameNotFoundException; import com.drinkless.oauth2.server.mapper.SysUserMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; /** * @Description: userDetailsService实现类 * @Date: 2020/11/30 17:12 */ @Slf4j @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserMapper sysUserMapper; @Autowired private HttpServletRequest request; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserMapper.selectByUsername(username); if (sysUser == null) { throw new MyUsernameNotFoundException(username + " is not exists!"); } if (!BCrypt.checkpw(request.getParameter("password"), sysUser.getPassword())) { throw new MyUsernameNotFoundException(username + " password error!"); } return sysUser; } } @Data public class SysUser implements UserDetails { /* 用户id */ private Long id; /* 用户名 */ private String username; /* 密码 */ private String password; private String nickname; private String province; private String serverNos; private String idCardNo; private Date birthDate; private String gender; private String phone; private Boolean status; private Boolean deleteFlag; private Date createDatetime; private Date updateDatetime; /* 角色列表 */ private List<SysRole> authorities = new ArrayList<>(); /* 指示是否未过期的用户的凭据(密码),过期的凭据防止认证 默认true 默认表示未过期 */ private boolean credentialsNonExpired = true; //账户是否未过期,过期无法验证 默认true表示未过期 private boolean accountNonExpired = true; //用户是未被锁定,锁定的用户无法进行身份验证 默认true表示未锁定 private boolean accountNonLocked = true; //是否可用 ,禁用的用户不能身份验证 默认true表示可用 private boolean enabled = true; }
登录成功处理器
package com.drinkless.oauth2.server.handlder; import com.fasterxml.jackson.databind.ObjectMapper; import com.drinkless.oauth2.server.entity.SysRole; import com.drinkless.oauth2.server.entity.SysUser; import com.drinkless.oauth2.server.enums.ResultEnum; import com.drinkless.oauth2.server.utils.ResultUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * @Description: 登录成功处理 * @Date: 2020/11/30 21:48 */ @Slf4j public class LoginSuccessHandler implements AuthenticationSuccessHandler { public final ObjectMapper MAPPER = new ObjectMapper(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { SysUser user = (SysUser) authentication.getPrincipal(); log.info("【登录】用户名:{}登录成功", user.getUsername()); log.info("【登录】权限:{}", user.getAuthorities()); // 返回权限 HashMap<Object, Object> map = new HashMap<>(); List<SysRole> authorities = user.getAuthorities(); List<String> roles = new ArrayList<>(); for (SysRole role : authorities) { roles.add(role.getAuthority()); } map.put("roles", roles); // 返回给前端 response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(MAPPER.writeValueAsString(ResultUtil.successResult(ResultEnum.SUCCESS_STATUS, map))); } }
登录失败处理器
package com.drinkless.oauth2.server.handlder; import com.fasterxml.jackson.databind.ObjectMapper; import com.drinkless.oauth2.server.entity.Result; import com.drinkless.oauth2.server.enums.ExceptionEnum; import com.drinkless.oauth2.server.utils.ResultUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 登录失败处理器 */ @Slf4j public class LoginFailureHandler implements AuthenticationFailureHandler { public final ObjectMapper MAPPER = new ObjectMapper(); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { Result result = ResultUtil.errorResult(ExceptionEnum.UNKNOWN_EXCEPTION.getCode(), "登录失败"); String username = request.getParameter("username"); if (e.getMessage().contains("is not exists") || e.getMessage().contains("password error")) { result = ResultUtil.errorResult(ExceptionEnum.UNKNOWN_EXCEPTION.getCode(), "用户或密码错误"); } response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(MAPPER.writeValueAsString(result)); log.error("【登录失败】用户名:{}", username); } }
登录超时 未登录处理器
package com.drinkless.oauth2.server.handlder; import com.fasterxml.jackson.databind.ObjectMapper; import com.drinkless.oauth2.server.entity.Result; import com.drinkless.oauth2.server.enums.HttpStatusEnum; import com.drinkless.oauth2.server.utils.ResultUtil; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Description: 登录超时 未登录处理器 * @Date: 2020/12/09 13:41 */ public class LoginExpireHandler implements AuthenticationEntryPoint { public final ObjectMapper MAPPER = new ObjectMapper(); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setStatus(HttpStatusEnum.UNAUTHORIZED.getCode()); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); Result result = ResultUtil.errorResult(HttpStatusEnum.UNAUTHORIZED.getCode(), "登录过期或未登录"); response.getWriter().write(MAPPER.writeValueAsString(result)); } }
登出成功处理
package com.drinkless.oauth2.server.handlder; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.databind.ObjectMapper; import com.drinkless.oauth2.server.constants.RedisConstant; import com.drinkless.oauth2.server.entity.Result; import com.drinkless.oauth2.server.enums.ResultEnum; import com.drinkless.oauth2.server.utils.JwtUtils; import com.drinkless.oauth2.server.utils.ResultUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContext; import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.web.context.support.WebApplicationContextUtils; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.concurrent.TimeUnit; /** * @Description: 登出成功处理 * @Date: 2020/12/14 13:31 */ @Slf4j public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler { public final ObjectMapper MAPPER = new ObjectMapper(); private TokenStore tokenStore; public LogoutSuccessHandler(TokenStore tokenStore) { this.tokenStore = tokenStore; } @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { try { // 获取用户信息 String header = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION); String token = header.substring(header.lastIndexOf("bearer") + 8); String tokenBody = JwtUtils.testJwt(token); JSONObject user = JSON.parseObject(tokenBody).getJSONObject("user"); // 清除redis中的token ServletContext context = httpServletRequest.getServletContext(); ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(context); ValueOperations valueOperations = (ValueOperations) ctx.getBean("valueOperations"); valueOperations.set(RedisConstant.REDIS_LOGIN + user.get("username"), "", 1, TimeUnit.MICROSECONDS); //返回退出成功信息 httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); Result result = ResultUtil.successResult(ResultEnum.SUCCESS_STATUS); PrintWriter out = httpServletResponse.getWriter(); out.write(MAPPER.writeValueAsString(result)); out.flush(); out.close(); } catch (Exception e) { e.printStackTrace(); } } }
找不到用户异常类
package com.drinkless.oauth2.server.exception; import org.springframework.security.core.AuthenticationException; /** * @Description: 找不到用户异常 * @Date: 2020/12/10 14:48 */ public class MyUsernameNotFoundException extends AuthenticationException { public MyUsernameNotFoundException(String msg) { super(msg); } public MyUsernameNotFoundException(String msg, Throwable t) { super(msg, t); } }
跨域
@Configuration public class CorsConfig implements WebMvcConfigurer { /** * 跨域访问配置 */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600) .allowedHeaders("*"); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。