目录
一、springsecurity密码模式项目实战
1、前言
a、整体框架spring-cloud-alibaba-nacos + spring-security + jwt + redis
2、认证服务器
a、认证服务器pom.xml
<!-- Spring Security、OAuth2 和JWT等 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- nacos 客户端 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- feign 调用服务接口 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springboot 2.3.x版本以上将validation单独抽取出来了,要我们自己引入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--mybatis-plus启动器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--Druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 配置处理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--lombok setter,getter-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- swagger-->
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
</dependency>
<!-- aliyun -->
<!-- aliyun oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!--http请求工具-->
<dependency>
<groupId>com.arronlong</groupId>
<artifactId>httpclientutil</artifactId>
</dependency>
<!-- 工具类依赖 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
b、bootstrap.yml和application.yml
spring:
application:
name: auth-server # 当前服务的应用名,与nacos中的dataid的前缀匹配
cloud:
nacos:
discovery:
server-addr: 172.21.25.56:8848 # 注册中心地址 nacos server
config:
server-addr: 172.21.25.56:8848 # 配置中心地址 nacos server
file-extension: yml # 配置中心的配置后缀
profiles:
active: dev # 指定环境为开发环境,即读取 auth-server-dev.yml
server:
port: 7001
servlet:
context-path: /auth # 上下文件路径,请求前缀 ip:port/article
spring:
redis:
host: 172.21.25.56
port: 6379
password: # redis不需要用户名
# 数据源配置
datasource:
username: root
password: 123456
url: jdbc:mysql://172.21.25.56:3306/dcy_blog_auth?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
#mysql8版本以上驱动包指定新的驱动类
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据源其他配置, 在 DruidConfig配置类中手动绑定
initialSize: 8
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
c、启动类
@EnableFeignClients // 扫描Feign接口
@EnableDiscoveryClient // 标识nacos客户端
@SpringBootApplication
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
d、安全配置类
/**
* 安全配置类
*/
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定使用自定义查询用户信息来完成身份认证
auth.userDetailsService(userDetailsService);
}
@Bean // 使用 password模块时需要此bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf攻击
http.formLogin()
// 成功处理器
// http://localhost:6001/auth/login
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.logout()
// http://localhost:6001/auth/logout
.logoutSuccessHandler(logoutSuccessHandler)
.and()
.csrf().disable();
}
}
@Service // 不一定不要少了
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IFeignSystemController feignSystemController;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 判断用户名是否为空
if (StringUtils.isEmpty(username)) {
throw new BadCredentialsException("用户名不能为空");
}
// 2. 通过用户名查询数据库中的用户信息
SysUser sysUser = feignSystemController.findUserByUsername(username);
if (sysUser == null) {
throw new BadCredentialsException("用户名或密码错误");
}
// 3. 通过用户id去查询数据库的拥有的权限信息
List<SysMenu> menuList =
feignSystemController.findMenuListByUserId(sysUser.getId());
// 4. 封装权限信息(权限标识符code)
List<GrantedAuthority> authorities = null;
if (CollectionUtils.isNotEmpty(menuList)) {
authorities = new ArrayList<>();
for (SysMenu menu : menuList) {
// 权限标识
String code = menu.getCode();
authorities.add(new SimpleGrantedAuthority(code));
}
}
// 5. 构建UserDetails接口的实现类JwtUser对象
JwtUser jwtUser = new JwtUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(),
sysUser.getNickName(), sysUser.getImageUrl(), sysUser.getMobile(), sysUser.getEmail(),
sysUser.getIsAccountNonExpired(), sysUser.getIsAccountNonLocked(),
sysUser.getIsCredentialsNonExpired(), sysUser.getIsEnabled(),
authorities);
return jwtUser;
}
}
@Data
public class JwtUser implements UserDetails {
@ApiModelProperty(value = "用户ID")
private String uid;
@ApiModelProperty(value = "用户名")
private String username;
@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "密码,加密存储, admin/1234")
private String password;
@ApiModelProperty(value = "昵称")
private String nickName;
@ApiModelProperty(value = "头像url")
private String imageUrl;
@ApiModelProperty(value = "注册手机号")
private String mobile;
@ApiModelProperty(value = "注册邮箱")
private String email;
// 1 true 0 false
@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "帐户是否过期(1 未过期,0已过期)")
private boolean isAccountNonExpired; // 不要写小写 boolean
@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "帐户是否被锁定(1 未过期,0已过期)")
private boolean isAccountNonLocked;
@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "密码是否过期(1 未过期,0已过期)")
private boolean isCredentialsNonExpired;
@JSONField(serialize = false) // 忽略转json
@ApiModelProperty(value = "帐户是否可用(1 可用,0 删除用户)")
private boolean isEnabled;
/**
* 封装用户拥有的菜单权限标识
*/
@JSONField(serialize = false) // 忽略转json
private List<GrantedAuthority> authorities;
// isAccountNonExpired 是 Integer 类型接收,然后转 boolean
public JwtUser(String uid, String username, String password,
String nickName, String imageUrl, String mobile, String email,
Integer isAccountNonExpired, Integer isAccountNonLocked,
Integer isCredentialsNonExpired, Integer isEnabled,
List<GrantedAuthority> authorities) {
this.uid = uid;
this.username = username;
this.password = password;
this.nickName = nickName;
this.imageUrl = imageUrl;
this.mobile = mobile;
this.email = email;
this.isAccountNonExpired = isAccountNonExpired == 1 ? true: false;
this.isAccountNonLocked = isAccountNonLocked == 1 ? true: false;
this.isCredentialsNonExpired = isCredentialsNonExpired == 1 ? true: false;
this.isEnabled = isEnabled == 1 ? true: false;
this.authorities = authorities;
}
}
/**
* 失败处理器:认证失败后响应json给前端
*/
@Component("customAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
// 响应错误信息:json格式
response.setContentType("application/json;charset=UTF-8");
String result = objectMapper.writeValueAsString(Result.error(e.getMessage()));
response.getWriter().write(result);
}
}
/**
* 成功处理器,校验客户端信息、生成jwt令牌,响应result数据
*/
@Component("customAuthenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
Logger logger = LoggerFactory.getLogger(getClass());
private static final String HEADER_TYPE = "Basic ";
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("登录成功 {}", authentication.getPrincipal());
// 获取请求头中的客户端信息
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
logger.info("header {}", header);
// 响应结果对象
Result result = null;
try {
if (header == null || !header.startsWith(HEADER_TYPE)) {
throw new UnsupportedOperationException("请求头中无client信息");
}
// 解析请求头的客户端信息
String[] tokens = RequestUtil.extractAndDecodeHeader(header);
assert tokens.length == 2;
String clientId = tokens[0];
String clientSecret = tokens[1];
// 查询客户端信息,核对是否有效
ClientDetails clientDetails =
clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
throw new UnsupportedOperationException("clientId对应的配置信息不存在:" + clientId);
}
// 校验客户端密码是否有效
if (!passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
throw new UnsupportedOperationException("无效clientSecret");
}
// 组合请求对象,去获取令牌
TokenRequest tokenRequest =
new TokenRequest(MapUtils.EMPTY_MAP, clientId,
clientDetails.getScope(), "custom");
OAuth2Request oAuth2Request =
tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication =
new OAuth2Authentication(oAuth2Request, authentication);
// 获取 访问令牌对象
OAuth2AccessToken accessToken =
authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
result = Result.ok(accessToken);
} catch (Exception e) {
logger.error("认证成功处理器异常={}", e.getMessage(), e);
result = Result.build(ResultEnum.AUTH_FAIL.getCode(), e.getMessage());
}
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
@Configuration
@EnableAuthorizationServer // 标识为认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean // 客户端使用jdbc管理
public ClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 配置被允许访问认证服务的客户端信息:数据库方式管理客户端信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
@Autowired // 在SpringSecurityConfig中已经添加到容器中了
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Resource
private TokenStore tokenStore;
@Resource
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Resource // 注入增强器
private TokenEnhancer jwtTokenEnhancer;
/**
* 关于认证服务器端点配置
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// /oauth/token post
// Basic Auth {username,password}客户端账号密码
// form-data {grant_type:password,username,password}密码认证模式,用户账号密码
// ********
// form-data {grant_type:refresh_token,refresh_token}刷新令牌
// 密码模块必须使用这个authenticationManager实例
endpoints.authenticationManager(authenticationManager);
// 刷新令牌需要 使用userDetailsService
endpoints.userDetailsService(userDetailsService);
// 令牌管理方式
endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
// 添加增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
// 组合 增强器和jwt转换器
List<TokenEnhancer> enhancerList = new ArrayList<>();
enhancerList.add(jwtTokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancerList);
endpoints.tokenEnhancer(enhancerChain).accessTokenConverter(jwtAccessTokenConverter);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// /oauth/check_token post
// Basic Auth {username,password}客户端账号密码
// form-data {token}
// 解析令牌,默认情况 下拒绝访问
security.checkTokenAccess("permitAll()");
}
}
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
public class RequestUtil {
public static String[] extractAndDecodeHeader(String header) throws IOException {
// `Basic ` 后面开始截取 clientId:clientSecret
byte[] base64Token = header.trim().substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException var8) {
throw new RuntimeException("请求头解析失败:" + header);
}
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if (delim == -1) {
throw new RuntimeException("请求头无效:" + token);
} else {
return new String[]{token.substring(0, delim), token.substring(delim + 1)};
}
}
}
/**
* 退出成功处理器,清除redis中的数据
*/
@Component("customLogoutSuccessHandler")
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private TokenStore tokenStore;
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// 获取访问令牌
String accessToken = request.getParameter("accessToken");
if (StringUtils.isNotBlank(accessToken)) {
OAuth2AccessToken oAuth2AccessToken =
tokenStore.readAccessToken(accessToken);
if (oAuth2AccessToken != null) {
// 删除redis中对应的访问令牌
tokenStore.removeAccessToken(oAuth2AccessToken);
}
}
// 退出成功,响应结果
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(Result.ok().toJsonString());
}
}
@Configuration
public class JwtTokenStoreConfig {
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter =
new JwtAccessTokenConverter();
// 采用非对称加密文件
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(
new ClassPathResource("oauth2.jks"), "oauth2".toCharArray());
converter.setKeyPair(factory.getKeyPair("oauth2"));
return converter;
}
@Autowired
private RedisTemplate redisTemplate;
@Bean
public TokenStore tokenStore() {
// 采用jwt管理信息
return new JwtTokenStore(jwtAccessTokenConverter()){
@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
// 将jwt中的唯一标识 jti 作为redis中的 key 值 ,value值是 accessToken 访问令牌
if(token.getAdditionalInformation().containsKey("jti")) {
String jti = token.getAdditionalInformation().get("jti").toString();
// 存储到redis中 (key, value, 有效时间,时间单位)
redisTemplate.opsForValue()
.set(jti, token.getValue(), token.getExpiresIn(), TimeUnit.SECONDS);
}
super.storeAccessToken(token, authentication);
}
@Override
public void removeAccessToken(OAuth2AccessToken token) {
if(token.getAdditionalInformation().containsKey("jti")) {
String jti = token.getAdditionalInformation().get("jti").toString();
// 将redis中对应jti的记录删除
redisTemplate.delete(jti);
}
super.removeAccessToken(token);
}
};
}
}
/**
* 扩展响应的认证信息
*/
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken,
OAuth2Authentication oAuth2Authentication) {
JwtUser user = (JwtUser) oAuth2Authentication.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put("userInfo", JSON.toJSON(user));
// 设置附加信息
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(map);
return oAuth2AccessToken;
}
}
e、中文认证提示信息
@Configuration
public class ReloadMessageConfig {
/**
* 加载中文的认证提示信息
*
* @return
*/
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages_zh_CN"); // 不要有后缀名.properties
return messageSource;
}
}
f、刷新令牌
@RestController
public class AuthController {
Logger logger = LoggerFactory.getLogger(getClass());
private static final String HEADER_TYPE = "Basic ";
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthService authService;
@GetMapping("/user/refreshToken") // localhost:7001/auth/user/refreshToken?refreshToken=xxxx
public Result refreshToken(HttpServletRequest request) {
try {
// 获取请求中的刷新令牌
String refreshToken = request.getParameter("refreshToken");
Preconditions.checkArgument(StringUtils.isNotEmpty(refreshToken), "刷新令牌不能为空");
// 获取请求头
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if(header == null || !header.startsWith(HEADER_TYPE)) {
throw new UnsupportedOperationException("请求头中无client信息");
}
// 解析请求头的客户端信息
String[] tokens = RequestUtil.extractAndDecodeHeader(header);
assert tokens.length == 2;
String clientId = tokens[0];
String clientSecret = tokens[1];
// 查询客户端信息,核对是否有效
ClientDetails clientDetails =
clientDetailsService.loadClientByClientId(clientId);
if(clientDetails == null) {
throw new UnsupportedOperationException("clientId对应的配置信息不存在:" + clientId);
}
// 校验客户端密码是否有效
if( !passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
throw new UnsupportedOperationException("无效clientSecret");
}
// 获取新的认证信息
return authService.refreshToken(header, refreshToken);
} catch(Exception e) {
logger.error("refreshToken={}", e.getMessage(), e);
return Result.error("新令牌获取失败:" + e.getMessage());
}
}
}
@Service // 不要少了
public class AuthService {
@Autowired // 负载均衡的client
private LoadBalancerClient loadBalancerClient;
/**
* 通过刷新令牌获取新的认证令牌
* @param header 请求头:客户端信息,Basic clientId:clientSecret
* @param refreshToken 刷新令牌
* @return
*/
public Result refreshToken(String header, String refreshToken) throws HttpProcessException {
// 采用客户端负载均衡,从Nacos服务器中获取对应服务的ip与端口号
ServiceInstance serviceInstance = loadBalancerClient.choose("auth-server");
if(serviceInstance == null) {
return Result.error("未找到有效认证服务器,请稍后重试");
}
// 请求刷新令牌url
String refreshTokenUrl = serviceInstance.getUri().toString() + "/auth/oauth/token";
// 封装刷新令牌请求参数
Map<String, Object> map = new HashMap<>();
map.put("grant_type", "refresh_token");
map.put("refresh_token", refreshToken);
// 构建配置请求头参数
Header[] headers = HttpHeader.custom() // 自定义请求
.contentType(HttpHeader.Headers.APP_FORM_URLENCODED) // 数据类型
.authorization(header) // 认证请求头(客户信息)
.build();
// 请求配置
HttpConfig config =
HttpConfig.custom().headers(headers).url(refreshTokenUrl).map(map);
// 发送请求,响应认证信息
String token = HttpClientUtil.post(config);
JSONObject jsonToken = JSON.parseObject(token);
// 如果响应内容中包含了error属性值,则获取新的认证失败。
if(StringUtils.isNotEmpty(jsonToken.getString("error"))) {
return Result.build(ResultEnum.TOKEN_PAST);
}
return Result.ok(jsonToken);
}
}
3、资源服务器
a、资源服务器pom.xml
<!-- Spring Security、OAuth2 和JWT等-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
b、资源服务器配置类
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别权限控制
@EnableResourceServer // 标识为资源服务器,请求资源接口时,必须在请求头带个access_token
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore); // jwt管理令牌
}
/**
* 资源服务器的安全配置
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement() // 采用token进行管理身份,而没有采用session,所以不需要创建HttpSession
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests() // 请求的授权配置
// 将 swagger接口文档相关的url放行
.antMatchers("/v2/api-docs", "/v2/feign-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources", "/swagger-resources/configuration/security",
"/swagger-ui.html", "/webjars/**").permitAll()
// 放行以 /api 开头的请求接口
.antMatchers("/api/**").permitAll()
// 所有请求都要有all范围权限
.antMatchers("/**").access("#oauth2.hasScope('all')")
// 其他请求都要通过身份认证
.anyRequest().authenticated();
}
}
@Configuration
public class JwtTokenStoreConfig {
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 采用的是非对称加密,资源服务器要使用公钥解密 public.txt
ClassPathResource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream(), "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
converter.setVerifierKey(publicKey);
// 将定义的转换器对象添加到jwt转换器中
converter.setAccessTokenConverter( new CustomAccessTokenConverter() );
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore( jwtAccessTokenConverter() );
}
/**
* 定制 AccessToken 转换器,为额外添加的用户信息在资源服务中获取
*/
private class CustomAccessTokenConverter extends DefaultAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
oAuth2Authentication.setDetails(map);
return oAuth2Authentication;
}
}
}
c、解决远程调用请求头丢失问题
/**
* 使用 Feign进行远程调用时,先经过此拦截器,在此拦截器中将请求头带上访问令牌
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// 通过RequestContextHolder工具来获取请求相关变量
ServletRequestAttributes attributes =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if(attributes != null) {
// 获取请求对象
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if(StringUtils.isNotEmpty(token)) { // Bearer xxx
// 在使用feign远程调用时,请求头就会带上访问令牌
requestTemplate.header(HttpHeaders.AUTHORIZATION, token);
}
}
}
}
d、获取当前用户
public class AuthUtil {
public static SysUser getUserInfo() {
Authentication authentication
= SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthenticationDetails details =
(OAuth2AuthenticationDetails)authentication.getDetails();
Map<String, Object> map = (Map<String, Object>) details.getDecodedDetails();
Map<String, String> userInfo = (Map<String, String>) map.get("userInfo");
SysUser user = new SysUser();
user.setId(userInfo.get("uid"));
user.setNickName(userInfo.get("nickName"));
user.setUsername( userInfo.get("username") );
user.setEmail( userInfo.get("email") );
user.setImageUrl( userInfo.get("imageUrl") );
user.setMobile( userInfo.get("mobile"));
return user;
}
}
4、网关服务器
a、网关服务器pom.xml
<!-- 解析 jwt -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>6.0</version>
</dependency>
b、根据请求头是否存在认证信息判断请求是否放行
@Component // 不要少了
public class AuthenticationFilter implements GlobalFilter, Ordered {
private static final String[] white = {"/api/"};
/**
* 定义验证请求头是否带有 Authorization
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 请求对象
ServerHttpRequest request = exchange.getRequest();
// 响应对象
ServerHttpResponse response = exchange.getResponse();
// /question/api/question/1
String path = request.getPath().pathWithinApplication().value();
// 公开api接口进行放行,无需认证
if (StringUtils.indexOfAny(path, white) != -1) {
// 直接放行
return chain.filter(exchange);
}
// 请求头信息
String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.isEmpty(authorization)) {
// 没有带authorization请求头,则响应错误信息
// 封装响应信息
JSONObject message = new JSONObject();
message.put("code", 1401);
message.put("message", "缺少身份凭证");
// 转换响应消息内容对象为字节
byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
// 设置响应对象状态码 401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
// 返回响应对象
return response.writeWith(Mono.just(buffer));
}
// 如果请求头不为空,则验证通过,放行此过滤器
return chain.filter(exchange);
}
@Override
public int getOrder() {
//过滤器执行顺序,越小越优先执行
return 0;
}
}
c、根据redis中token存储状态判断请求是否放行
@Component // 不要少了
public class AccessTokenFilter implements GlobalFilter, Ordered {
Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 校验请求头中的令牌是否有效,查询redis中是否存在 ,不存在则无效jwt
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 请求对象
ServerHttpRequest request = exchange.getRequest();
// 响应对象
ServerHttpResponse response = exchange.getResponse();
// 获取token
String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
String token = StringUtils.substringAfter(authorization, "Bearer ");
if (StringUtils.isEmpty(token)) {
// 如果为空,可能是白名单的请求,则直接放行
return chain.filter(exchange);
}
// 响应错误信息
String message = null;
try {
JWSObject jwsObject = JWSObject.parse(token);
JSONObject jsonObject = jwsObject.getPayload().toJSONObject();
// 校验redis中是否存在对应jti的token
String jti = jsonObject.get("jti").toString();
// 查询是否存在
Object value = redisTemplate.opsForValue().get(jti);
if (value == null) {
logger.info("令牌已过期 {}", token);
message = "您的身份已过期, 请重新认证!";
}
} catch (ParseException e) {
logger.error("解析令牌失败 {}", token);
message = "无效令牌";
}
if (message == null) {
// 如果令牌存在,则通过
return chain.filter(exchange);
}
// 响应错误提示信息
JSONObject result = new JSONObject();
result.put("code", 1401);
result.put("message", message);
// 转换响应消息内容对象为字节
byte[] bits = result.toJSONString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
// 设置响应对象状态码 401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
// 返回响应对象
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
// 这个AccessTokenFilter过滤器在 AuthenticationFilter 之后执行
return 10;
}
}