微信公众号:[0error] 关注可了解更多的知识干货,也可看看生活杂谈。如有问题或建议,欢迎在公众号留言。
上篇我们讲了OAuth2.0的概念
这一篇针对实战详细说说
项目主体框架采用SpringCloudAlibaba和相关的生态技术组件
项目主目录
首先创建一个maven类型的项目,前面创建项目的步骤就略过咯。
首先填写pom.xml中的依赖,核心的就是Springframework的security和SpringCloudAlibaba的依赖了
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.5.3</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.0</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR3</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
子模块auth
准备好auth模块的pom文件
主要涉及到nacos和oauth2的包
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</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>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.15</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
<dependency>
<groupId>com.janeroad</groupId>
<artifactId>anduin-common</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.janeroad</groupId>
<artifactId>anduin-user</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
实体类UserLoginVO
实现org.springframework.security.core.userdetails
的接口UserDetails
enabled、clientId、authorities都是必须要有的
@Data
public class UserLoginVO implements UserDetails {
/**
* user_info.id
*/
private Integer infoId;
/**
* 手机号
*/
private String mobilePhone;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 创建时间
*/
private Long createTime;
/**
* 创建人
*/
private Integer createUser;
/**
* 最后修改时间
*/
private Integer updateTime;
/**
* 修改人
*/
private Integer updateUser;
/**
* 用户状态
*/
private Integer enabled;
/**
* 登录客户端ID
*/
private String clientId;
/**
* 权限数据
*/
private Collection<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
if (enabled.intValue() == 0){
return false;
}else {
return true;
}
}
}
JWT内容增强器
1、通过keytool生成jks文件
运行下面的命令生成私钥,姓名国家啥的可以不填
keytool -genkey -alias janeroad -keyalg RSA -keysize 1024 -keystore janeroad-jwt.jks -validity 365 -keypass janeroad -storepass janeroad
在上面的命令中,-alias选项为别名,-keypass和-storepass为密码选项,-validity为配置jks文件的过期时间(单位:天)。
输入密码之后生成公钥可私钥
然后将生成的jks文件放到resources目录
2、需要实现org.springframework.security.oauth2.provider.token
的TokenEnhancer接口
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
UserLoginVO userLogin = (UserLoginVO) authentication.getPrincipal();
Map<String, Object> info = new HashMap<>();
//把用户ID设置到JWT中
info.put("id", userLogin.getInfoId());
info.put("client_id",userLogin.getClientId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
认证服务器配置类
1、使用@EnableAuthorizationServer注解开启
2、keyPair()通过jks文件获取公钥来生成token
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private final PasswordEncoder passwordEncoder;
@Autowired
private final UserLoginServiceImpl userDetailsService;
@Autowired
private final AuthenticationManager authenticationManager;
@Autowired
private final JwtTokenEnhancer jwtTokenEnhancer;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("admin-app")//配置client_id 管理员端
.secret(passwordEncoder.encode("xxxx"))//设置自己的加密密钥
.scopes("all")
.authorizedGrantTypes("password", "refresh_token")//配置grant_type,表示授权类型
.accessTokenValiditySeconds(3600*24)//配置访问token的有效期
.refreshTokenValiditySeconds(3600*24*7)//配置刷新token的有效期
.and()
.withClient("user-app")//配置client_id 用户端
.secret(passwordEncoder.encode("xxxx"))//设置自己的加密密钥
.scopes("all")//配置申请的权限范围
.authorizedGrantTypes("password", "refresh_token")//配置grant_type,表示授权类型
.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"), "janeroad".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "xxxx".toCharArray());//密码
}
}
SpringSecurity配置类
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.antMatchers("/rsa/publicKey").permitAll()
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
OAuth2异常处理类
@ControllerAdvice
public class Oauth2ExceptionHandler {
@ResponseBody
@ExceptionHandler(value = OAuth2Exception.class)
public CommonResult handleOauth2(OAuth2Exception e) {
return CommonResult.failed(e.getMessage());
}
}
Oauth2获取Token返回信息封装
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
public class Oauth2TokenDTO {
private String token;
private String refreshToken;
private String tokenHead;
private int expiresIn;
}
登录功能的实现
@Service
public class UserLoginServiceImpl extends ServiceImpl<UserLoginMapper, UserLogin> implements UserDetailsService {
@Autowired
private UserLoginMapper userLoginMapper;
@Autowired
private IUserInfoService userInfoservice;
@Autowired
private HttpServletRequest request;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
String clientId = request.getParameter("client_id");
UserLoginVO userLoginVO = getByUserName(username);
if (Objects.isNull(userLoginVO)) {
throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
}
userLoginVO.setClientId(clientId);
String password = BCrypt.hashpw(userLoginVO.getPassword());
userLoginVO.setPassword(password);
if (!userLoginVO.isEnabled()) {
throw new DisabledException(MessageConstant.ACCOUNT_DISABLED);
} else if (!userLoginVO.isAccountNonLocked()) {
throw new LockedException(MessageConstant.ACCOUNT_LOCKED);
} else if (!userLoginVO.isAccountNonExpired()) {
throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED);
} else if (!userLoginVO.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED);
}
return userLoginVO;
}
/**
* 根据用户名查用户登录VO
* @param username
* @return
*/
private UserLoginVO getByUserName(String username){
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("username", username);
UserLogin userLogin = userLoginMapper.selectOne(queryWrapper);
String permissionsName = userInfoservice.getPermissionsByUserId(userLogin.getInfoId());
UserLoginVO userLoginVO = Convert.convert(UserLoginVO.class, userLogin);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(permissionsName));
userLoginVO.setAuthorities(authorities);
return userLoginVO;
}
}
子模块gateway
认证管理类
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
private RedisTemplate 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));
// }
// request.getHeaders().set("userId", userDto.getId().toString());
if (AuthConstant.USER_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 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));
}
}
资源服务器配置
@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);
}
}
全局过滤器
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@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();
JSONObject userObject = JSONObject.parseObject(userStr);
log.info("AuthGlobalFilter.filter() user:{}",userStr);
ServerHttpRequest request = exchange.getRequest().mutate().header(AuthConstant.USER_TOKEN_HEADER, userStr).header(AuthConstant.USER_ID_HEADER, userObject.get("id").toString()).build();
exchange = exchange.mutate().request(request).build();
} catch (ParseException e) {
e.printStackTrace();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
白名单路径访问时的处理
@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);
}
}
使用演示
1、调用网关层的登录,client_id选用用户界面的id,并非管理员界面的id

2、携带token请求用户界面接口

3、携带token请求管理员界面的接口
4、如果登录失效,使用refresh_token重新登陆
原文始发于微信公众号(0error):实战SpringSecurity+OAuth2
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/21943.html