基于OAuth2.0+JWT的鉴权中心(上——认证中心配置)
在上一讲中我们讲了项目的大门gateway的配置。我们一般的业务流程应该是用户访问服务,经过网关被拦截,然后通过鉴权中心进行认证,认证结束发放令牌,用户拿着令牌去访问资源,因此这一讲我们主要讨论一下鉴权认证。因为我用的是SpringCloud(2.1.6) 的Greenwich版本,所以我们本篇文章就是基于OAuth2.0+JWT,由于篇幅过长所以我分为两部分来讲,第一部分是认证中心配置我们也称为认证服务器,下一篇是资源中心配置,也叫资源服务器配置。
一)创建项目
创建模块主要分为两部分,认证服务器authorization-server模块和资源服务器app-customer-login模块。
A、authorization-server创建
这个模块主要是
1、创建一个新的模块,和前面几讲的一样,打开上一篇创建的项目microservice选中,并单击右键New→Module,选择Spring Initializr默认下一步;
2、进入该页面填Group和Artifact,Group要写对和上次的要一样,Artifact 我命名为authorization-server,如图1所示:
3、点击Next,选择相关依赖,Spring Cloud Security 下的Cloud OAuth2是核心依赖如下图2所示:
4、继续Next→Finish,等待下载依赖。完成后,首先进行启动类AuthorizationServerApplication的配置,如下图3所示:
@EnableResourceServer这个注解要注意,它是用来开启安全管控的,因为后面如果我们要继承WebSecurityConfigurerAdapter来完善更加详细的规则配置。
5、首先我们先建三个文件夹,分别为com.glen.authorizationserver.config、com.glen.authorizationserver.entity和com.glen.authorizationserver.service,如下图所示:
6、接下来我们配置AuthorizationServerApplication,其中@SpringBootApplication和@EnableDiscoveryClient这是基础注解,不再赘述。@EnableFeignClients这是在做负载均衡时讲过的。@EnableResourceServer这个注解比较特殊, 它用于开启资源服务配置,会配置资源服务相关的安全配置。
7、配置application.properties,如下图所示,具体信息看注释。
#微服务配置
spring.application.name=auth-server
server.port=8085
eureka.client.serviceUrl.defaultZone=http://localhost:8081/eureka/
# 数据库JPA配置
# Mysql驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 数据库地址 serverTimezone=UTC要写,不然有时会报错
spring.datasource.url=jdbc:mysql://localhost:3306/springauth?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
# 用户名
spring.datasource.username=root
# 密码
spring.datasource.password=glen1996
# 每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新
spring.jpa.hibernate.ddl-auto=update
# 是否打印sql语句
spring.jpa.show-sql=true
# 当遇到同样名字的时候,是否允许覆盖注册
spring.main.allow-bean-definition-overriding=true
# security用户名
spring.security.user.name=glen
# security密码
spring.security.user.password=123456
# 鉴权配置 配置token获取合验证时的策略
security.oauth2.authorization.check-token-access=permitAll()
# 暴露地址
management.endpoints.web.exposure.include=*
8、在com.glen.authorizationserver.entity下创建Role.java和User.java,这两个文件主要是角色和用户信息。
Role.java中的内容:
@Entity
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getAuthority() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
User.java中的内容:
@Entity
public class User implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column
private String password;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> authorities;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
9、在com.glen.authorizationserver.java中创建UserRepository.java和UserServiceDetail.java两个文件。
UserRepository.java
UserRepository这个类继承了JpaRepository,这个类封装了很多方法,jpa和mybatis这一点有点类似,如果有用过Mybatis的同学就会比较熟,项目中的findByUsername是自定义的方法,具体可以查看jpa的官方文档,这个方法主要是用来做查询,它和数据库的user表对应,后面我们会具体讨论数据库的问题。
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
UserServiceDetail.java
UserServiceDetail实现了UserDetailsService 这个接口的方法,返回了查询结果。
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username);
}
}
10、接下来我们要配置认证服务器,这一块是核心内容。新建两个java文件。OAuth2Config.java和WebSecurityConfig.java
A、OAuth2Config.java
OAuth2Config这个class主要是配置客户端信息和认证信息,认证信息主要是包含了JWT的配置,JWT个人感觉还是比session要好很多,JWT最大的优势还是在于通过无状态、可扩展的方式处理用户请求以及不用访问数据库去获取凭证,咱也可以不用redis。
JWT主要分为三部分Header(进行编码,用于指定加密算法)、Payload(进行编码)和Signature(即签名,它由编码的header和payload,使用用户指定的密钥secret,采用header中指定的哈希算法生成)
@Slf4j
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Autowired
private UserServiceDetail userServiceDetail;
@Autowired
private ClientDetailsService clientDetailsService;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("test-jwt.jks"), "test123".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("test-jwt"));
// converter.setSigningKey("123");
log.info("private key:"+converter);
return converter;
}
@Bean // 声明 ClientDetails实现
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
String finalSecret =new BCryptPasswordEncoder().encode("123456");
clients.inMemory()
// 配置一个客户端
.withClient("user-service")
.secret("123456")
// 配置客户端的域
.scopes("service")
// 配置验证类型为refresh_token和password
.authorizedGrantTypes("refresh_token","password")
// 配置token的过期时间为1h
.accessTokenValiditySeconds(3600 * 1000)
.resourceIds("user-service")
.authorities("ROLE_ADMIN");
// clients.withClientDetails(clientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 配置token的存储方式为JwtTokenStore
endpoints
//存储jwt
.tokenStore(tokenStore())
// 配置用于JWT私钥加密的增强器
.tokenEnhancer(jwtTokenEnhancer())
// 配置安全认证管理
.authenticationManager(authenticationManager);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单认证
security.tokenKeyAccess("permitAll()") //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
.checkTokenAccess("isAuthenticated()") //url:/oauth/check_token allow check token
.allowFormAuthenticationForClients();
}
}
以下是相关的方法说明:
a、tokenStore():tokenStore方法返回一个TokenStore对象的子对象JwtTokenStore,认证服务器取值给授权服务器配置器,通俗点就是让MyAuthorizationServerConfig能注入到值。
b、jwtTokenEnhancer():这个方法主要是用来设置认证服务器和资源服务器之间的密钥对,常见的有对称加密和非对称加密两种。converter.setSigningKey(“123”)这是对称加密方法,如果这里这样设置,资源服务器里也需要这样设置; converter.setKeyPair()这是非对称的加密方法。这一块要注意两个问题:
(1)、生成用于Token加密的私钥文件test-jwt.jks。jks文件的生成我们一般使用java自带的java keytool工具,只要环境变量配置没问题,输入如下命令:
keytool -genkeypair -alias test-jwt -validity 3650 -keyalg RSA -dname "CN=jwt,OU=jtw,O=jtw,L=zurich,S=zurich,C=CH" -keypass test123 -keystore test-jwt.jks -storepass test123</font>
回车即可,
-keypass test123:这是设置别名
-keystore test-jwt.jks:这是设置文件名
-storepass test123:这是设置密码,
如下图所示:
表示已经成功,在如图当前目录下即可查到该文件。
并将该文件放在resources目录下面
接下来我们运行如下命令,输入密码test123获取publicKey,这个后面要用:
keytool -list -rfc --keystore test-jwt.jks | openssl x509 -inform pem -pubkey
新建一个txt文件命名为public.cert,我们复制里面的publicKey到该文件中。
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsTuQdiX2yhgNLLHgUxjc
zher2JC63l3/DQAdRq/TX9harNie25KOZua9/CF+9XKNjKlJsecCj/UUo6ICAiTB
lOxbLjj/bIoIMefn/xtQNtPMPcZr5XdMmZRHW2kHg6y5yGDEYXWFzEtqXrULfUMP
CwV6vSoqxi6xj6i9wQPdCOCZiBBxWZfOwir4oDN/kD0eulCHd9H1l1SBtdRDGFeE
QBUjFMrDhurXUjYWay3021bGU2SbVUS1Av+qvpSmEQ7pRxf18QOtKD5Z2yP3EdS0
0jme/K84DDqbyUo3Zboh4YFz8OIXNrtjXFgPd3MrotTzlBr2teR4twF4zd01voQr
wQIDAQAB
-----END PUBLIC KEY-----
(2)、要在pom.xml文件中配置如下代码:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>cert</nonFilteredFileExtension>
<nonFilteredFileExtension>jks</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
目的是为了防止jks文件在maven编译时乱码,如果这个文出问题后面可能会导致无权限访问的问题。
c、clientDetailsService():
d、configure(ClientDetailsServiceConfigurer clients) :这个方法主要是配置客户端,具体看注解,重点说一下楼主遇到的坑,要设置一下.resourceIds(“user-service”)和.authorities(“ROLE_ADMIN”),不然后面访问的时候可能会报如下错误:
{
"error": "access_denied",
"error_description": "Invalid token does not contain resource id (oauth2-resource)"
}
resourceIds这个的值随便设总要要前后文上下文等等保持一致就行,数据库也一样,我这里设的的clientid一样只是为了方便……嗯,没错,只是为了方便……
e、configure(AuthorizationServerEndpointsConfigurer endpoints):这个方法主要是配置token的存储方式。
f、configure(AuthorizationServerSecurityConfigurer security) :这个是允许表单认证。
B、WebSecurityConfig.java
WebSecurityConfig类主要是继承WebSecurityConfigurerAdapter类,
代码如下:
@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@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/token","/oauth/**").permitAll();
}
@Autowired
UserServiceDetail userServiceDetail;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
}
}
下面对各个方法做下说明:
a、passwordEncoder():这个方法类似于格式化工具方法,因为spring security升级后有所改变。如果加密方式不对,很有可能报以下错误:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
b、authenticationManagerBean():
这个bean,在OAuth2Config文件中有引用到。
c、configure(HttpSecurity http):这里在配置权限,防止令牌获取不被通过,所以允许了oauth开头的所有路径。
d、configure(AuthenticationManagerBuilder auth):这个方法还是去数据库查询用户的密码,做权限验证。
好了本篇文章就讲到这里,下篇文章我们讲基于OAuth2.0+JWT的认证中心(下——资源中心配置)
SpringCloud从零构建(一)——Eureka注册中心
SpringCloud从零构建(二)——创建服务端Server
SpringCloud从零构建(三)——创建消费者Customer
SpringCloud从零构建(四)——Feign实现负载均衡
SpringCloud从零构建(五)——Config配置中心
SpringCloud从零构建(六)——消息总线Bus+Rabbit MQ实现动态刷新
SpringCloud从零构建(七)——网关中心(gateway)配置
SpringCloud从零构建(八)——基于OAuth2.0+JWT的鉴权中心(上——认证中心配置)
github地址:https://github.com/gjen1996/microservice
如果有问题欢迎小伙伴留言和我沟通交流。