携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
auth模块 pom.xml引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
- UserDetails => Spring Security基础接口,包含某个用户的账号,密码,权限,状态(是否锁定)等信息。只有getter方法。
- Authentication => 认证对象,认证开始时创建,认证成功后存储于SecurityContext
- principal => 用户信息对象,是一个Object,通常可转为UserDetails
UserDetails接口
用于表示一个principal,但是一般情况下是作为(你所使用的用户数据库)和(Spring Security 的安全上下文需要保留的信息)之间的适配器。
实际上就是相当于定义一个规范,Security这个框架不管你的应用时怎么存储用户和权限信息的。只要你取出来的时候把它包装成一个UserDetails对象给我用就可以了。
@Data
@Builder
public class SecurityUser implements UserDetails, Principal {
/** 统一默认为ROLE_USER */
private static final String AUTHORITY = "ROLE_USER";
private static final long serialVersionUID = 1443123970566148983L;
/** 唯一主键id */
private final String id;
/** 密码 */
private final String phone;
private String username;
private String password;
/*
*/
/** email *//*
private final String email;
*/
/** 身份证号 *//*
private final String idCardNo;
*/
private Boolean isEnabled;
private Collection<SimpleGrantedAuthority> authorities;
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
/** 以下四个函数,都可以根据一些用户字段添加判别逻辑,非常灵活 */
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return isEnabled;
}
@Override
public String getName() {
return this.getUsername();
}
}
UserDetailsService
认证的操作,框架都已经帮你实现了,它所需要的只是,你给我提供获取信息的方式。所以它就定义一个接口,然后让你去实现,实现好了之后再注入给它。
框架提供一个UserDetailsService接口用来加载用户信息。如果要自定义实现的话,用户可以实现一个CustomUserDetailsService的类,
然后把你的应用中的UserService和AuthorityService注入到这个类中,用户获取用户信息和权限信息,
然后在loadUserByUsername方法中,构造一个User对象(框架的类)返回即可。
框架提供的UserDetailsService接口默认实现
InMemoryDaoImpl => 存储于内存
JdbcDaoImpl => 存储于数据库(磁盘)
其中,如果你的数据库设计符合JdbcDaoImpl中的规范,你也就不用自己去实现UserDetailsService了。
但是大多数情况是不符合的,因为你用户表不一定就叫users,可能还有其他前缀什么的,比如叫tb_users。或者字段名也跟它不一样。
如果你一定要使用这个JdbcDaoImpl,你可以通过它的setter方法修改它的数据库查询语句。 它是利用Spring框架的JdbcTemplate来查询数据库的
注入到认证处理类中的,框架利用AuthenticationManager(接口)来进行认证。而Security为了支持多种方式认证,它提供ProviderManager类,这个实现了AuthenticationManager接口。
它拥有多种认证方式,可以根据认证的类型委托给对应的认证处理类进行处理,这个处理类实现了AuthenticationProvider接口。
所以,最终UserDetailsService是注入到AuthenticationProvider的实现类中。
UserDetailService只单纯地负责存取用户信息,除了给框架内的其他组件提供数据外没有其他功能。而认证过程是由AuthenticationManager来完成的。
(大多数情况下,可以通过实现AuthenticationProvider接口来自定义认证过程)
@Service
public class SecurityUserServiceImpl implements UserDetailsService {
/** 这里用自定义数据举例,后续可通过数据库获取用户信息 */
private static final List<SecurityUser> mockUsers;
static {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 这里密码必须加密
String pwd = passwordEncoder.encode("asdf");
mockUsers = new ArrayList<>();
SecurityUser user = SecurityUser.builder()
.id("001")
.username("hy")
.password(pwd)
.authorities(List.of(new SimpleGrantedAuthority("ADMIN")))
.isEnabled(true)
.build();
mockUsers.add(user);
}
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
Optional<SecurityUser> user = mockUsers.stream().filter(u -> u.getUsername().equals(userName)).findFirst();
if (user.isEmpty()) {
throw new UsernameNotFoundException(BusinessCode.USERNAME_PASSWORD_ERROR.getMsg());
}
SecurityUser securityUser = user.get();
// 下面抛出的异常 Spring Security 会自动捕获并进行返回
if (!securityUser.isEnabled()) {
throw new DisabledException(BusinessCode.ACCOUNT_DISABLED.getMsg());
} else if (!securityUser.isAccountNonLocked()) {
throw new LockedException(BusinessCode.ACCOUNT_LOCKED.getMsg());
} else if (!securityUser.isAccountNonExpired()) {
throw new AccountExpiredException(BusinessCode.ACCOUNT_EXPIRED.getMsg());
} else if (!securityUser.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(BusinessCode.CREDENTIALS_EXPIRED.getMsg());
}
return securityUser;
}
}
WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter是构建SecurityFilterChain的关键,在WebSecurityConfigurerAdapter的init方法中会创建一个SecurityBuilder类型的实例对象【HttpSecurity】并保存到WebSecurity的securityFilterChainBuilders属性中,后续通过SecurityBuilder来完成SecurityFilterChain的创建
URL强制拦截保护服务,可以配置哪些路径不需要保护,哪些需要保护。默认全都保护
\
- 继承了WebSecurityConfigurerAdapter之后,再加上几行代码,我们就能实现以下的功能:
- 1、要求用户在进入你的应用的任何URL之前都进行验证
- 2、创建一个用户名是“user”,密码是“password”,角色是“ROLE_USER”的用户
- 3、启用HTTP Basic和基于表单的验证
- 4、Spring Security将会自动生成一个登陆页面和登出成功页面
- @EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security。
- 继承了WebSecurityConfigurerAdapter之后,再加上几行代码,我们就能实现以下的功能:
- 1、要求用户在进入你的应用的任何URL之前都进行验证
- 2、创建一个用户名是“user”,密码是“password”,角色是“ROLE_USER”的用户
- 3、启用HTTP Basic和基于表单的验证
- 4、Spring Security将会自动生成一个登陆页面和登出成功页面
- 默认页面:
- 登录页面:/login
- 注销页面:/login?logout
- 错误页面:/login?error
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 系统安全用户验证模式:
* 1、使用内存模式创建验证
* 2、使用数据库创建验证,实现userDetailsService接口即可
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
/**
* 不定义没有password grant_type即密码授权模式
* (总共四种授权模式:授权码、implicat精简模式、密码、client credentials)
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
// spring security 5.0 之后默认实现类改为 DelegatingPasswordEncoder 此时密码必须以加密形式存储
return new BCryptPasswordEncoder();
}
/**
* 如果有要忽略拦截校验的静态资源,在此处添加
* 忽略任何以”/resources/”开头的请求,这和在XML配置http@security=none的效果一样
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
}
AuthorizationServerConfigurerAdapter
AuthorizationServerConfigurerAdapter: 配置OAuth授权服务器的工作方式
使用spring Security OAuth2模块创建授权服务器,
需要使用注解@EnableAuthorizationServer并扩展AuthorizationServerConfigurerAdapter类
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* EnableAuthorizationServer: 通过该注解暴露OAuth的鉴权接口 /oauth/token 等
* 这里的 AuthenticationManager 和 PasswordEncoder 都是在上面的 WebSecurityConfig 中配置过的
*/
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private SecurityUserServiceImpl userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 进行本条设置以后 参数可以在form-data设置,而不必要在Authorization设置了
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 通过client_id可以区分不同客户端,可用于后续的自定义鉴权
.withClient("portal")
// 密码必须加密
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("password", "refresh_token")
.scopes("webclient")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(3600*5);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
// 配置获取用户信息
.userDetailsService(userService);
}
}
简单测试
启动auth服务,调用接口 http://localhost:9000/oauth/token
优化认证服务
使用JWT加强token
在AuthorizationServerConfig添加相关配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
// 设置token转换器
.accessTokenConverter(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
@Bean
public KeyPair keyPair() {
// 这里的password需要与之前创建时输入的一致,否则会无法读取导致服务启动失败
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt");
}
简单测试
报错:class path resource [jwt.jks] cannot be opened because it does not exist
原因:编译不成功,文件没有在target下
暂时手动加文件复制到target/classes/young/ 目录下
调用接口
JWT中添加自定义字段
AuthorizationServerConfig中设置实现TokenEnhancer接口进行内容增强,
在其中添加一些自定义字段,如在Token中添加userId字段
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
// 设置token转换器
.accessTokenConverter(accessTokenConverter());
// 将两个增强器连起来
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.tokenEnhancer(tokenEnhancerChain);
}
/** JWT内容增强,在其中添加一些自定义字段 */
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
Map<String, Object> additionalInfo = new HashMap<>();
SecurityUser user = (SecurityUser) authentication.getPrincipal();
additionalInfo.put("userId", user.getId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}
调用接口测试成功
自定义oauth/token 返回数据结构
SpringSecurity提供的auth接口如下
故只需要自定义认证接口,在接口内调用该函数,
将返回的OAuth2AccessToken包装为自定义的数据结构,即可具体地定义返回数据结构
@RestController
@RequestMapping("/oauth")
public class AuthController {
@Autowired
private TokenEndpoint tokenEndpoint;
@PostMapping(value = "/token")
public CommonResult<TokenDTO> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
TokenDTO tokenDTO = TokenDTO.builder().token(oAuth2AccessToken.getValue())
.refreshToken(oAuth2AccessToken.getRefreshToken().getValue())
.tokenHead("Bearer ").build();
return CommonResult.success(tokenDTO);
}
}
-
调用gateway路由auth:/auth/oauth/token 获取token失败
- 没有放行这个接口,配置WhiteListUrlsConfig
-
Caused by: java.text.ParseException: Missing required "keys" member 解析失败
- 经测试,直接调用认证模块可以返回,但是从网关调用就失败。因为所有返回参数被包装了,所以无法解析到公钥的keys