一、私钥公钥生成
1.使用java提供的keytool证书管理工具生成公钥私钥证书
创建一个文件夹,在该文件夹下执行如下命令:
keytool -genkeypair -alias kaikeba -keyalg RSA -keypass kaikeba -keystore kaikeba.jks -storepass kaikeba
- -alias:密钥的别名
- -keyalg:使⽤的hash算法
- -keypass:密钥的访问密码
- -keystore:密钥库⽂件名
- -storepass:密钥库的访问密码
密钥库证书文件:
2.导出公钥
使用工具:openssl (这是一个加解密工具包,用来导出公钥信息)
安装 openssl:http://slproweb.com/products/Win32OpenSSL.html
下载完成之后,将openssl安装路径bin的路径(例如D:\OpenSSL-Win64\bin)配置到操作环境系统变量path中:
cmd进入kaikeba.jks文件所在目录执行如下命令
keytool -list -rfc --keystore kaikeba.jks | openssl x509 -inform pem -pubkey
若是报“openssl不是内部或外部命令,也不是可运行的程序和批处理文件”,则重启一下电脑
重启之后再执行命令,就可以得到私钥和公钥
将上边的公钥拷贝到文本public.key文件中,然后放到资源服务器中
3.测试:
(1)在pom.xml文件中引入oauth2依赖
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
(2)将准备好的证书文件和公钥放至resourses目录下
(3)测试代码:
package com.kaikeba.t31;
public class JwtTest {
//⽣成⼀个jwt令牌
@Test
public void testCreateJwt() throws Exception {
//证书⽂件
String key_location = "kaikeba.jks";
//密钥库密码
String keystore_password = "kaikeba";
//访问证书路径
ClassPathResource resource = new ClassPathResource(key_location);
//密钥⼯⼚
KeyStoreKeyFactory keyStoreKeyFactory = new
KeyStoreKeyFactory(resource, keystore_password.toCharArray());
//密钥的密码,此密码和别名要匹配
String keypassword = "kaikeba";
//密钥别名
String alias = "kaikeba";
//密钥对(密钥和公钥)
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,
keypassword.toCharArray());
//私钥
RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();
//定义payload信息
Map<String, Object> tokenMap = new HashMap<String, Object>();
tokenMap.put("id", "123");
tokenMap.put("name", "malong");
tokenMap.put("roles", "r01,r02");
tokenMap.put("ext", "1");
//⽣成jwt令牌
Jwt jwt = JwtHelper.encode(new ObjectMapper().writeValueAsString(tokenMap), new RsaSigner(aPrivate));
//取出jwt令牌
String token = jwt.getEncoded();
System.out.println(token);
}
//资源服务使⽤公钥验证jwt的合法性,并对jwt解码
@Test
public void testVerify() {
//jwt令牌
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHQiOiIxIiwicm9sZXMiOiJyMDEscjAyIiwibmFtZSI6Im1hbG9uZyIsImlkIjoiMTIzIn0.NTaHZjQ91gugf2RnfLgexNi_uFlvRxkNexxDSldW8niW18I4Y_iOEp76FvODR28UwFJuaO38ZGxbroHLCq8wt4oQb6xKTPu_eRqdUmk2cJAno2i43w3ynUMCdecOHFPBxxWMfVKEGMcgfKKS81bXjrf-0EL0WEA9i0u03Fuu87dg8SI8aZkH1VP60X-CAuHOICLTB2AjOijWgX1OQr52kIiQ4OQ_C_16tRvsSMxarQ_RkjdTZ-3UD2AF6mXPr8rGuJR1ZV-0xjSm4QDej2pKUYS8Qkj_6KUTI_CRTNN9DLCBfRMO71ZG8WbeFfVP7PxOO5s986hPU_9ywXcC-sqMpA";
//公钥
String publickey = "-----BEGIN PUBLIC KEY-----\n" +
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfg3iq3+hsUJKi/MWXrd\n" +
"/1xxVzdfMbg8Jk7hxLSgRtjb4XzptUqXaRy4dq+SYTmr+An6qD0qnDDBV4MdrEW/\n" +
"Gk9o31KSHRKLVpv3NCd2QWJCtuTPLBJYOneHDGifiTR8B4nA7FMYuT83FWH/WQYy\n" +
"mUQSp+nEwrNfF/W0glAIlWitFABuPXS6dkh8veoTPFNWTaewp7E4lv+bGCQBlMZw\n" +
"4Lcbw8fVWnUZ2s9ByjtFGFHAof5QZA7HTDGs/flXVkgm8UvSRwB2fo8ibBU0aLD6\n" +
"FYaOmtix9NocxISnGGjdlJ7rVSXygh/TkE2ZFAtN/2E1C1CXd3wWJapvdRqjZtVl\n" +
"twIDAQAB\n" +
"-----END PUBLIC KEY-----";
//校验jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));
//获取jwt原始内容
String claims = jwt.getClaims();
System.out.println(claims);
try {
Map<String, String> map = new ObjectMapper().readValue(claims, Map.class);
System.out.println(map.get("user_name"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行上面的testCreateJwt()测试,里面的JwtHelper.encode方法就是生成令牌的方法。在console中显示的就是令牌。
将此令牌复制粘贴到测试方法testVerify()中的token中,将公钥信息粘贴到publickey变量中,然后执行该测试方法。
执行成功:
二、用户角色管理:
需求分析:授权中心在发令牌的时候需要确认身份,要判断用户的用户名、密码、角色等信息进行处理和查询,验证用户的用户名、密码,得到用户角色等信息,并存储在jwt里面,然后才能交给springSecurity进行授权
实现:基于RBAC模型
RBAC(基于角色的权限控制role base access control)是一种设计模式,是用来设计和管理权限相关数据的一种模型
1.搭建授权中心工程
(1)配置pom.mxl文件
<?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-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>t31-auth-center</artifactId>
<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>
(2)配置文件
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
application.yml
spring:
application:
name: auth-center
profiles:
active: dev
(3)将之前生成的密码对文件拷贝进来
(4)编写启动类
package com.kaikeba.t31;
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;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
2.配置类
(1)OAuth2配置类
package com.kaikeba.t31.config;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);
@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());
tokenServices.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(30)); // 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()");
}
}
(2)SpringSecurity配置类
package com.kaikeba.t31.config;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration 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 {
http.requestMatchers().anyRequest()
.and().authorizeRequests().antMatchers("/oauth/**").permitAll();//开放/oauth/开头的所有请求
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//注⼊⾃定义的UserDetailsService,采⽤BCrypt加密
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
3.UserDetailsService
这个组件是扩展spring security最重要的一个组件。作用是从数据库中读取rbac数据,用户、密码、角色数据返回UserDetails,交给Spring Security框架匹配验证,办法令牌等操作。
package com.kaikeba.t31.service.impl;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
private static final Logger logger =
LoggerFactory.getLogger(UserDetailServiceImpl.class);
@Autowired
UserClient userClient;
@Autowired
PasswordEncoder passwordEncoder;//BCryptPasswordEncoder
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AdminUser user = userClient.getByUserName(username);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (user != null) {
logger.debug("current user = " + user);
//获取⽤户的授权
List<AdminRole> roles = userClient.selectRolesByUserId(user.getId());
//声明授权⽂件
for (AdminRole role : roles) {
if (role != null && role.getName() != null) {
GrantedAuthority grantedAuthority = new
SimpleGrantedAuthority("ROLE_" + role.getName());//spring Security中权限名称必须满⾜ROLE_XXX
grantedAuthorities.add(grantedAuthority);
}
}
}
logger.debug("granted authorities = " + grantedAuthorities);
return new User(user.getUserName(), user.getPassword(), grantedAuthorities);
}
}
其中读取数据的UserFeign代码如下
package com.kaikeba.t31.client;
@FeignClient(name = "admin-service", fallback = UserClient.UserClientFallback.class)
public interface UserClient extends UserApi {
@Component
@RequestMapping("/fallback")//这个可以避免容器中requestMapping重复
class UserClientFallback implements UserClient {
private static final Logger LOGGER = LoggerFactory.getLogger(UserClientFallback.class);
@Override
public AdminUser getByUserName(String userName) {
LOGGER.info("异常发生,进入fallback方法");
return null;
}
@Override
public List<AdminRole> selectRolesByUserId(Long id) {
LOGGER.info("异常发生,进入fallback方法");
return null;
}
}
}
package com.kaikeba.t31.admin.api;
public interface UserApi {
@ApiOperation("通过登录获得用户")
@GetMapping("/get/{userName}")
AdminUser getByUserName(@PathVariable("userName") String userName);
@ApiOperation("通过用户ID获得角色")
@GetMapping("/select-roles/{id}")
List<AdminRole> selectRolesByUserId(@PathVariable("id") Long id);
}
配置完了之后就可以启动授权中心了。
4.搭建资源中心
(1)JwtConfig.java 创建TokenStore配置使用公钥验证令牌
package com.kaikeba.t31.config;
@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)ResourceServerConfiguration 配置资源服务器
package com.kaikeba.t31.config;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration 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.yaml
用于 资源服务器在启动的时候都需要对公钥进行一次验签,验签通过才能启动。本工程在nacos中进行此配置。