搭建认证中心
依赖
eureka-client,openfeign,oauth2+web为必选依赖
eureka-client提供服务注册与发现
openfeign提供远程调用
oauth2+web实现基于认证中心
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</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-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
实现基于rbac的认证模型(核心代码)
/**
* 根据用户名查询用户信息
*/
User findByUsername(String username);
/**
* 根据用户id查询用户角色
*/
List<Role> findByUserId(Long userId);
/**
* 根据角色id查询权限
*/
List<Permission> findByRoleIds(List<Long> roleIds);
整合Security实现RBCA认证
UserDetails接口 认证用户信息 UserDetailsService接口 认证用户信息服务
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SecurityUser extends User implements UserDetails {
private Collection<? extends GrantedAuthority> authorities;
/**
* security 所需用户的角色或权限
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return super.getPassword();
}
@Override
public String getUsername() {
return super.getUsername();
}
/**
* 账号是否过期
*/
@Override
public boolean isAccountNonExpired() {
return super.getIsAccountNonExpired() == 1 ;
}
/**
* 账号是否锁定
*/
@Override
public boolean isAccountNonLocked() {
return super.getIsAccountNonLocked() == 1;
}
/**
* 密码是否过期
*/
@Override
public boolean isCredentialsNonExpired() {
return super.getIsCredentialsNonExpired() == 1;
}
/**
* 账号是否禁用
*/
@Override
public boolean isEnabled() {
return super.getIsEnabled() == 1;
}
}
@Slf4j
@Service
public class SecurityUserService implements UserDetailsService {
@Resource
private IUserService iUserService;
@Resource
private IRoleService iRoleService;
@Resource
private IPermissionService iPermissionService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = iUserService.findByUsername(userName);
if(ObjectUtils.isEmpty(user)){
throw new UsernameNotFoundException("用户名不存在");
}
List<Role> roleList = iRoleService.findByUserId(user.getId());
List<Long> roleIds = roleList.stream().map(Role::getId).collect(Collectors.toList());
List<Permission> permissionList = iPermissionService.findByRoleIds(roleIds);
List<GrantedAuthority> authorities = new ArrayList<>();
roleList.forEach(role-> authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName())));
permissionList.forEach(permission -> authorities.add(new SimpleGrantedAuthority(permission.getCode())));
SecurityUser securityUser = new SecurityUser();
BeanUtils.copyProperties(user,securityUser);
securityUser.setAuthorities(authorities);
log.info("当前登录的用户信息:{}",securityUser);
return securityUser;
}
}
Security认证配置
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* security5.x 必须指定加密方式
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Resource
private UserDetailsService userDetailsService;
/**
* 基于数据库的认证和授权
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**+
* 密码模式支持
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Token服务配置
@Configuration
public class TokenConfig {
/**
* 基于rsa的非对此加密 加密的密钥
*/
@Value("${token.signing_key}")
public static String SIGNING_KEY;
@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;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
认证服务器配置
/**
* 认证服务器配置类
*/
@Configuration
@EnableAuthorizationServer // 开启了认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
private DataSource dataSource;
@Resource
private AuthenticationManager authenticationManager;
@Resource
private UserDetailsService userDetailsService;
@Resource
private TokenStore tokenStore;
@Resource
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
public ClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
@Bean
public AuthorizationCodeServices jdbcAuthorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// password
endpoints.authenticationManager(authenticationManager);
// 刷新令牌
endpoints.userDetailsService(userDetailsService);
// 令牌的管理方式
endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
// 授权码管理策略
endpoints.authorizationCodeServices(jdbcAuthorizationCodeServices());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 认证后可访问 /oauth/token_key , 默认拒绝访问
security.tokenKeyAccess("permitAll()");
// 认证后可访问 /oauth/check_token , 默认拒绝访问
security.checkTokenAccess("isAuthenticated()");
}
}
配置文件
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
instance:
instance-id: ${spring.application.name}${server.port}
prefer-ip-address: true
server:
port: 9000
spring:
application:
name: auth-center
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://192.168.70.136:3306/oauth2?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
mybatis-plus:
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
token:
signing_key: xxxxxxxxxx
启动类
@EnableEurekaClient
@SpringBootApplication(exclude = {RedisAutoConfiguration.class})
public class AuthCenterApp {
public static void main(String[] args) {
SpringApplication.run(AuthCenterApp.class,args);
}
}
搭建网关统一鉴权
依赖
zull 网关,eureka,oauth2必选依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
跨域访问配置
/**
* 跨域
*/
@Configuration
public class GatewayConfig {
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedMethod("*");
//↓核心代码
corsConfiguration.addExposedHeader("Authorization");
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
}
Security安全配置
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
}
Token解析服务
@Configuration
public class TokenConfig {
@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(), StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
converter.setVerifierKey(publicKey);
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
资源服务管理配置
@Configuration
public class ResourceServerConfig {
public static final String RESOURCE_ID = "product-server";
@Resource
private TokenStore tokenStore;
@Configuration
@EnableResourceServer
public class AuthResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
}
@Configuration
@EnableResourceServer
public class Product1ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/product1/**").access("#oauth2.hasScope('PRODUCT_API')");
}
}
@Configuration
@EnableResourceServer
public class Product2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID).tokenStore(tokenStore);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/product2/**");
// .access("#oauth2.hasScope('PRODUCT_API')");
}
}
}
JwtToken令牌黑名单
可采用redis使用比较优雅的处理,这里不做处理选择简单的单机抛异常拉黑阻止路由转发
@RestController("/jwt")
public class JwtController {
public final static Set<Authentication> jwtSet = new HashSet<>();
@GetMapping("/jwtBlacklist")
public String jwtBlacklist(@AuthenticationPrincipal Authentication authentication){
jwtSet.add(authentication);
for (Authentication authentication1: jwtSet) {
System.out.println("黑名单"+jwtSet);
}
return "添加成功";
}
}
Token令牌传递过滤器
/**
* 请求资源前,先通过此 过滤器进行用户信息解析和校验 转发
*/
@Slf4j
@Component
public class AuthenticationFilter extends ZuulFilter {
Logger logger = LoggerFactory.getLogger(getClass());
@Override
public String filterType() {
return "pre"; //前置处理
}
@Override
public int filterOrder() {
return 0; //最高优先级
}
@Override
public boolean shouldFilter() {
return true; //是否启用
}
@SneakyThrows
@Override
public Object run() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if( !(authentication instanceof OAuth2Authentication)) {
return null;
}else if(JwtController.jwtSet.contains(authentication)){
System.out.println("拉入黑名单");
throw new RuntimeException("禁止访问");
}else {
RequestContext context = RequestContext.getCurrentContext();
Map<String, Object> stringObjectMap = authenticationToMap(authentication);
System.out.println("\n\n"+stringObjectMap+"\n\n");
// 将用户信息和权限信息转成json,再通过base64进行编码
byte[] header = new ObjectMapper().writeValueAsBytes(stringObjectMap);
String base64 = Base64Utils.encodeToString(header);
context.addZuulRequestHeader("auth-token", base64);
}
return null;
}
private Map<String,Object> authenticationToMap(Authentication authentication){
Map<String, Object> result = new HashMap<>();
result.put("principal", authentication.getPrincipal()); //用户名
result.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities())); //角色和权限信息
result.put("details", authentication.getDetails());//ip地址等其他信息
return result;
}
}
配置文件
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
instance:
instance-id: ${spring.application.name}${server.port}
prefer-ip-address: true
server:
port: 80
spring:
application:
name: zuul-gateway
zuul:
sensitive-headers: null
add-host-header: true
routes:
authentication:
path: /auth/**
serviceId: auth-center
stripPrefix: true //去除前缀
product1:
path: /product1/**
serviceId: product1-server
stripPrefix: true
product2:
path: /product2/**
serviceId: product2-server
stripPrefix: true
启动类
@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class ZuulGatewayApp {
public static void main(String[] args) {
SpringApplication.run(ZuulGatewayApp.class,args);
}
}
资源服务器搭建
依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</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-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
资源服务器配置
TokenConfig 令牌解析 ResourceServerConfig 资源服务器配置 TokenAuthenticationFilter token获取 手动认证
@Configuration
public class TokenConfig {
public static final String SIGNING_KEY = "xxxxxxx";
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
ClassPathResource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream(), StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
converter.setVerifierKey(publicKey);
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Resource
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.resourceId(TokenConstant.RESOURE_ID).tokenStore(tokenStore).stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(new TokenAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests().anyRequest().authenticated();
}
}
/**
* 获取网关转发过来的请求头中保存的明文token值,用户信息
*/
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authToken = httpServletRequest.getHeader(TokenConstant.TOKEN_NAME);
if(StringUtils.isNotEmpty(authToken)) {
String authTokenJson = new String(Base64Utils.decodeFromString(authToken));
ObjectMapper objectMapper = new ObjectMapper();
Map hashMap = objectMapper.readValue(authTokenJson, Map.class);
Object principal = hashMap.get(TokenConstant.PRINCIPAL);
Object details = hashMap.get(TokenConstant.DETAILS);
ArrayList authorities = (ArrayList) hashMap.get(TokenConstant.AUTHORITIES);
List<GrantedAuthority> grantedAuthorities = new ArrayList<> ();
authorities.forEach(authorite->grantedAuthorities.add(new SimpleGrantedAuthority(authorite.toString())));
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(principal,null,grantedAuthorities);
usernamePasswordAuthenticationToken.setDetails(details);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
Fegin配置
请求携带请求头信息
支持https
public class FeignBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
requestTemplate.header(name, values);
}
}
}
}
@Configuration
public class FeignHeaderSupportConfig {
/**
* feign请求拦截器
*
* @return
*/
@Bean
public RequestInterceptor requestInterceptor(){
return new FeignBasicAuthRequestInterceptor();
}
}
public class FeignHttpSupportConfig{
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public Client getClient() throws KeyManagementException, NoSuchAlgorithmException {
SSLContext sslContext = SSLContext.getInstance("tls");
final TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[]{};
}
}
};
sslContext.init(null, trustAllCerts, new SecureRandom());
return new Client.Default(sslContext.getSocketFactory(), null);
}
}
配置文件
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
instance:
instance-id: ${spring.application.name}${server.port}
prefer-ip-address: true
server:
port: 9002
spring:
application:
name: product2-server
启动类和测试代码
@FeignClient(value = "product1-server",configuration = {FeignHeaderSupportConfig.class, FeignHttpSupportConfig.class})
public interface Product1Client {
@GetMapping
String get();
}
@RestController
@EnableFeignClients
@EnableEurekaClient
@SpringBootApplication(exclude = {RedisAutoConfiguration.class, DataSourceAutoConfiguration.class})
public class Product2ServerApp {
public static void main(String[] args) {
SpringApplication.run(Product2ServerApp.class, args);
}
@GetMapping
public String test(@AuthenticationPrincipal Authentication authentication) {
System.out.println("Product2ServerApp访问成功");
System.out.println(authentication);
return "Product2ServerApp";
}
@PreAuthorize("hasAuthority('sys:user:list')")
@GetMapping("/userlist")
public String string() {
return "恭喜你 拥有sys:user:list权限";
}
@PreAuthorize("hasAuthority('xxxx')")
@GetMapping("/testAA")
public String test() {
return "这个权限不开放";
}
@Resource
Product1Client product1Client;
@GetMapping("/fegin")
public String aa(){
String s = product1Client.get();
return "远程调用成功";
}
}
测试
推荐使用密码模式获取token进行测试
通过网关获取token
http://localhost/auth/oauth/token
通过网关校验token
http://localhost:9000/oauth/check_token