SpringCloud 整合 Spring Security 认证鉴权【SpringCloud系列6】
SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发本文章是系列文章中的一篇本文章实现的是 auth-api 生成令牌的功能。
SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见。
程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCloud 微服务系列项目开发
本文章是系列文章中的一篇
- 1、SpringCloud 项目基础工程搭建 【SpringCloud系列1】
- 2、SpringCloud 集成Nacos注册中心 【SpringCloud系列2】
- 3、SpringCloud Feign远程调用 【SpringCloud系列3】
- 4、SpringCloud Feign远程调用公共类抽取 【SpringCloud系列4】
- 5、SpringCloud 整合Gateway服务网关 【SpringCloud系列5】
本文章实现的是 auth-api 生成令牌的功能
1 创建一个 SpringBoot 基础项目
创建方式在这里有详细说明 SpringBoot项目创建【SpringBoot系列1】
然后在 SpringCloud 项目的父 pom.xml 中添加 module
然后在 auth-api 服务中添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
application.yml 配置如下
server:
port: 7001
spring:
application:
name: '@project.name@'
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
创建一个测试 Controller
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 早起的年轻人
* @version 1.0
**/
@RestController
@RequestMapping("/test/auth")
public class AuthTestController {
@RequestMapping(value = "/index")
public String tedtIndex() {
return " 测试 auth 访问 ";
}
}
启动 auth-api 服务 7001 端口,然后浏览器中访问测试接口
访问会被拦截至一个登录页面
添加 Security 的配置如下:
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author 早起的年轻人
* @description 安全管理配置
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager =
new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
return inMemoryUserDetailsManager;
}
//密码编码器:不加密
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//授权规则配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权配置
.antMatchers("/login").permitAll() //登录路径放行
.anyRequest().authenticated() //其他路径都要认证之后才能访问
.and().formLogin() //允许表单登录
.successForwardUrl("/loginSuccess") // 设置登陆成功页
.and().logout().permitAll() //登出路径放行
.and().csrf().disable(); //关闭跨域伪造检查
}
}
然后重启服务,在登录页面 输入这里配置的用户名 zs 与密码 123 登录成功后就可以正常访问。
2 实现数据库中的用户登录访问
修改 WebSecurityConfig 配置文件如下:
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author 早起的年轻人
* @description 安全管理配置
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
// @Bean
// public UserDetailsService userDetailsService(){
// InMemoryUserDetailsManager inMemoryUserDetailsManager =
// new InMemoryUserDetailsManager();
// inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
// return inMemoryUserDetailsManager;
// }
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//密码编码器:不加密
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//授权规则配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权配置
.antMatchers("/login").permitAll() //登录路径放行
.anyRequest().authenticated() //其他路径都要认证之后才能访问
.and().formLogin() //允许表单登录
.successForwardUrl("/loginSuccess") // 设置登陆成功页
.and().logout().permitAll() //登出路径放行
.and().csrf().disable(); //关闭跨域伪造检查
}
}
然后自定义一个 UserDetailsService 的实现类来查询登录用户的信息
@Slf4j
@Component
public class UserServiceImpl implements UserDetailsService {
/**
* 查询用户服务的 Feign
*/
@Resource
private FeignUserClient feignUserClient;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//根据用户ID来查询用户信息
UserInfo userInfo = feignUserClient.queryByUserName(s);
if (userInfo==null) {
return null;
}
//定义权限
String[] authorities = {"admin"};
//用户的密码
String password = userInfo.getPassword();
//扩展存储用户信息
Map<String,Object> map = new HashMap<>();
map.put("userName",s);
map.put("userId",userInfo.getId());
//转JSON
String jsonString = JSON.toJSONString(map);
//构建
UserDetails userDetails =
User.withUsername(jsonString)
.password(password).authorities(authorities).build();
return userDetails;
}
}
我这里边调用了 FeignUserClient 来调用用户服务查询用户详情,大家也可以修改成自己的查询数据库用户方法。
这里重新构建了 UserDetails ,相当于是把用户的密码校验放给了 Spring Security 去做。
3 使用密码模式获取登录令牌
如下图所示,访问 oauth/token 接口,输入 用户名与密码来获取 access_token 。
3.1 AuthenticationManager 与 PasswordEncoder
实现密码模式认证,需要配置 AuthenticationManager 与 PasswordEncoder ,修改 WebSecurityConfig 如下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author 早起的年轻人
* @description 安全管理配置
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
// @Bean
// public UserDetailsService userDetailsService(){
// InMemoryUserDetailsManager inMemoryUserDetailsManager =
// new InMemoryUserDetailsManager();
// inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
// return inMemoryUserDetailsManager;
// }
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//密码编码器:不加密
@Bean
public PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
//授权规则配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权配置
.antMatchers("/oauth/**", "/login/**","/logout/**")
.permitAll() //登录路径放行
.anyRequest().authenticated() //其他路径都要认证之后才能访问
.and().formLogin() //允许表单登录
.and().logout().permitAll() //登出路径放行
.and().csrf().disable(); //关闭跨域伪造检查
}
}
oauth2 的 授权服务器配置
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.token.AuthorizationServerTokenServices;
import javax.annotation.Resource;
/**
* @author 早起的年轻人
* @description 授权服务器配置
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Resource(name = "authorizationServerTokenServicesCustom")
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired
private AuthenticationManager authenticationManager;
//客户端详情服务
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()// 使用in-memory存储
.withClient("XcWebApp")// client_id
.secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥
.resourceIds("all")//资源列表
.authorizedGrantTypes("password", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允许的授权范围
.autoApprove(false);//false跳转到授权页面
}
//令牌端点的访问配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)//认证管理器
.tokenServices(authorizationServerTokenServices)//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//令牌端点的安全配置
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()") //oauth/token_key是公开
.checkTokenAccess("permitAll()") //oauth/check_token公开
.allowFormAuthenticationForClients() //表单认证(申请令牌)
;
}
}
然后需要配置一下 TokenStore 的生成策略
/**
* @author 早起的年轻人
* @version 1.0
**/
@Configuration
public class TokenStoreConfig {
private String SIGNING_KEY = "mq123";
@Autowired
TokenStore tokenStore;
@Bean
public TokenStore tokenStore() {
//使用内存存储令牌(普通令牌)
return new InMemoryTokenStore();
}
//令牌管理服务
@Bean(name = "authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
}
4 启动项目 获取token
postman 访问 http://localhost:7001/oauth/token
需要注意 认证模式这里的配置 username 与 password 对应上述配置中的
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()//基于内存配置
//客户端ID
.withClient("XcWebApp")
//密钥
.secret(new BCryptPasswordEncoder().encode("XcWebApp"))
...
}
然后请求参数
- grant_type : password 固定写法 密码模式
- scope : 授权范围,与上述配置中一至
- username 与 password 就是你数据库中的用户数据
获取 access_token 成功后 ,可以调用 /oauth/check_token 接口校验
可以刷新 token
http://localhost:7001/oauth/token
参数类型固定
- grant_type 取值为 refresh_token
- refresh_token 取值为 获取 token 时返回的 refresh_token 值
5 整合 JWT 来生成令牌
修改 TokenStoreConfig 配置如下:
@Configuration
public class TokenStoreConfig {
private String SIGNING_KEY = "qwert.123456";
@Autowired
TokenStore tokenStore;
// @Bean
// public TokenStore tokenStore() {
// //使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
//令牌管理服务
@Bean(name = "authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
}
再次访问获取 token 接口,可以发现 access_token 值有改变
6 使用 Redis 来保存令牌
上述内容中,生成的令牌保存在内存里,服务一重启就失效了,所以在实际应用开发中,一般将令牌保存在 Redis 中或者数据库中,本项目中是保存在 Redis 中,认证服务中添加redis 的依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
然后 application.yml 中添加 redis 配置如下:
server:
port: 7001
spring:
application:
name: '@project.name@'
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
redis:
database: 0 # Redis数据库索引(默认为0)
host: localhost # Redis服务器地址
port: 6379 # Redis服务器连接端口
password: 12345678 # Redis服务器连接密码(默认为空)
然后添加 RedisTokenStore 的配置如下
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
}
再重启服务,再次获取 token 发现生成的token保存在redis中
本项目源码 https://gitee.com/android.long/spring-cloud-biglead/tree/master/biglead-api-06-auth
如果有兴趣可以关注一下公众号 biglead ,每周都会有 java、Flutter、小程序、js 、英语相关的内容分享
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)