这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战
这次课程主要是实践,这里记录一下搭建auth-center的过程
准备:生成密钥证书
第1步:生成密钥证书
下面的命令生成密钥证书,采用RSA算法,每个证书包含公钥和私钥
-- 创建一个文件夹,在该文件夹下执行如下命令
keytool -genkeypair -alias kaikeba -keyalg RSA -keypass kaikeba -keystore kaikeba.jks -storepass kaikeba
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名
-storepass:密钥库的访问密码
复制代码
生成证书文件kaikeba.jks,搭建授权服务器时用
第2步:证书管理
-- 查询证书信息
keytool -list -keystore kaikeba.jks
-- 删除别名
keytool -delete -alias kaikeba -keystore kaikeba.jsk
复制代码
第3步:导出公钥
keytool -list -rfc --keystore kaikeba.jks | openssl x509 -inform pem -pubkey
复制代码
将公钥保存到public.key文件,搭建资源服务器时用
搭建授权中心
第1步:创建授权中心模块
t31-auth-center
第2步:添加依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>t31-parent</artifactId>
<groupId>com.kaikeba</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>t31-auth-center</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.kaikeba</groupId>
<artifactId>t31-admin-instance</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>jks</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
</project>
复制代码
第3步:配置
application.yml
spring:
application:
name: auth-center
profiles:
active: dev
复制代码
bootstrap.yml
spring:
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yaml
extension-configs[0]:
data-id: common.yaml
refresh: true
extension-configs[1]:
data-id: db.yaml
refresh: true
# 多个接口上的@FeignClient(“相同服务名”)会报错,overriding is disabled。
# 设置 为true ,即 允许 同名
main:
allow-bean-definition-overriding: true
复制代码
生成的密钥证书放到resources下
第4步:启动器
AuthApplication.java
package com.kaikeba.t31.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 授权中心
*
* @author yangdc
* @date 2021/11/22
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
复制代码
第5步:OAuth2配置
package com.kaikeba.t31.auth.config;
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.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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 org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;
/**
* OAuth2配置
*
* @author yangdc
* @date 2021/11/22
*/
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
//证书路径和密钥库密码
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("kaikeba.jks"), "kaikeba".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//密钥别名
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("kaikeba"));
return converter;
}
@Bean
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//配置通过表oauth_client_details,读取客户端数据
clients.withClientDetails(clientDetailsService());
}
/**
* 配置token service和令牌存储方式(tokenStore
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//tokenStore
endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtAccessTokenConverter()).authenticationManager(authenticationManager);
//tokenService
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(false);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// 30天
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单认证
security.allowFormAuthenticationForClients()
//放行oauth/token_key(获得公钥)
.tokenKeyAccess("permitAll()")
//放行 oauth/check_token(验证令牌)
// .checkTokenAccess("isAuthenticated()");
.checkTokenAccess("permitAll()");
}
}
复制代码
第6步:Spring Security配置
package com.kaikeba.t31.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Security配置
*
* @author yangdc
* @date 2021/11/22
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 注入自定义UserDetailService,读取rbac数据
*/
@Autowired
private UserDetailsService userDetailsService;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开放/oauth/开头的所有请求
http.requestMatchers().anyRequest()
.and().authorizeRequests().antMatchers("/oauth/**").permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 注入自定义的UserDetailsService,采用BCrypt加密
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
复制代码
第7步:UserDetailService
UserDetailService作用是从数据库中读取rbac数据,用户、密码、角色数据返回UserDetails,交给Spring Security框架匹配验证,颁发令牌等操作
package com.kaikeba.t31.auth.service.impl;
import com.kaikeba.t31.admin.po.Role;
import com.kaikeba.t31.auth.client.UserClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 用户详情服务实现
* 作用:读取数据库用户角色数据
*
* @author yangdc
* @date 2021/11/22
*/
@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UserClient userClient;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 使用OpenFeign调用admin-service,得到用户角色数据
com.kaikeba.t31.admin.po.User user = userClient.getByUsername(username);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (user != null) {
log.debug("current user = " + user);
// 获取用户的授权
List<Role> roles = userClient.selectRoleByUserId(user.getId());
// 声明授权文件
for (Role role : roles) {
if (role != null && role.getName() != null) {
// 封闭用户角色信息,必须ROLE_开头
// spring Security中权限名称必须满足ROLE_XXX
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
grantedAuthorities.add(grantedAuthority);
}
}
}
log.debug("granted authorities = " + grantedAuthorities);
// 返回Spring Security框架的User对象
return new User(user.getUserName(), user.getPassword(), grantedAuthorities);
}
}
复制代码
读取数据的UserFeign如下
package com.kaikeba.t31.auth.client;
import com.kaikeba.t31.admin.po.Role;
import com.kaikeba.t31.admin.po.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
/**
* 用户客户端
*
* @author yangdc
* @date 2021/11/22
*/
@FeignClient(name = "admin-service", fallback = UserClient.UserClientFallback.class)
public interface UserClient extends UserApi {
@Slf4j
@Component
//这个可以避免容器中requestMapping重复
@RequestMapping("/fallback")
class UserClientFallback implements UserClient {
@Override
public User getByUsername(String username) {
log.info("异常发生,进入fallback方法");
return null;
}
@Override
public List<Role> selectRoleByUserId(Long id) {
log.info("异常发生,进入fallback方法");
return null;
}
}
}
复制代码
搭建资源中心
t31-admin-service服务
第0步:配置
先前public.key复制到resources下
第1步:Jwt配置
package com.kaikeba.t31.admin.config;
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.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
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 org.springframework.util.FileCopyUtils;
import java.io.IOException;
/**
* Jwt配置
*
* @author yangdc
* @date 2021/11/22
*/
@Configuration
public class JwtConfig {
public static final String public_cert = "public.key";
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource(public_cert);
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
}catch (IOException e) {
throw new RuntimeException(e);
}
// 设置校验公钥
converter.setVerifierKey(publicKey);
// 设置证书签名密码,否则报错
converter.setSigningKey("kaikeba");
return converter;
}
}
复制代码
第2步:资源服务器配置
package com.kaikeba.t31.admin.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.TokenStore;
/**
* 资源服务器配置
*
* @author yangdc
* @date 2021/11/22
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
// .antMatchers("/**").permitAll();
.antMatchers("/user/**").permitAll()
// 用于测试
.antMatchers("/book/**").hasRole("ADMIN")
.antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}
}
复制代码
第3步:security.yml
所有资源中心都要配置oauth/token_key,用来验证公钥,启动时验证
resources/security.yml
security:
oauth2:
resource:
jwt:
#如果使用JWT,可以获取公钥用于 token 的验签
key-uri: http://localhost:9098/oauth/token_key
复制代码
第4步:权限控制说明
Spring Security中定义了四个支持权限控制的表达式注解,分别是
- @PreAuthorize
- @PostAuthorize
- @PreFilter
- @PostFilter
其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤.在需要控制权限的方法上, 我们可以添加@PreAuthorize注解,用于方法执行前进行权限检查,校验用户当前角色是否能访问该方法.
1 开启EnableGlobalMethodSecurity
使用上述注解控制权限需要设置EnableGlobalMethodSecurity -----见第2步资源服务器配置
2 如何使用注解
package com.kaikeba.t31.admin.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
/**
* @author yangdc
* @date 2021/11/22
*/
@Slf4j
@RestController
public class TestController {
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "product id : " + id;
}
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
return "order id : " + id;
}
@GetMapping("/book/{id}")
public String getBook(@PathVariable String id) {
return "book id : " + id;
}
@GetMapping("/anno/{id}")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String getAnno(@PathVariable String id) {
return "admin id :" + id;
}
@RequestMapping("/hello")
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
public String hello() {
return "hello you ...";
}
@GetMapping("/getPrinciple")
public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication, Principal principal, Authentication authentication) {
log.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
log.info(oAuth2Authentication.toString());
log.info("principal.toString() " + principal.toString());
log.info("principal.getName() " + principal.getName());
log.info("authentication: " + authentication.getAuthorities().toString());
return oAuth2Authentication;
}
}
复制代码