1.Spring Security 是基于 Spring 的身份认证(Authentication)和用户授权(Authorization)框架,提供了一 套 Web 应用安全性的完整解决方案。其中核心技术使用了 Servlet 过滤器、IOC 和 AOP 等。
2.什么是身份认证
身份认证指的是用户去访问系统资源时,系统要求验证用户的身份信息,用户身份合法才访问对应资源。 常见的身份认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
3.什么是用户授权
当身份认证通过后,去访问系统的资源,系统会判断用户是否拥有访问该资源的权限,只允许访问有权限的 系统资源,没有权限的资源将无法访问,这个过程叫用户授权。
比如 会员管理模块有增删改查功能,有的用户只能进行查询,而有的用户可以进行修改、删除。一般来说, 系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
父级工程:mengxuegu-security-parent
子模块:mengxuegu-security-base – 基础模块
mengxuegu-security-core – 核心依赖,springSecurtiy相关配置
mengxuegu-security-web – web服务模块,用于项目启动
依赖关系:web依赖core,
core依赖base
package com.mengxuegu.security.config;
import com.mengxuegu.security.authentication.CustomAuthenticationFailureHandler;
import com.mengxuegu.security.authentication.mobile.MobileAuthenticationConfig;
import com.mengxuegu.security.authentication.mobile.MobileValidateFilter;
import com.mengxuegu.security.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.builders.WebSecurity;
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;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 安全控制中心
*
* @Author wangzw
* @Date 2022/10/12 14:01
*/
@Configuration
@EnableWebSecurity // 启动SpringSecurity过滤器链功能
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启注解方法级权限控制
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private MobileValidateFilter mobileValidateFilter;
@Autowired
private UserDetailsService customUserDetailsService;
@Autowired
private AuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private MobileAuthenticationConfig mobileAuthenticationConfig;
Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public PasswordEncoder passwordEncoder() {
// 设置默认的加密方式
return new BCryptPasswordEncoder();
}
/**
* 身份认证管理器
* 1、认证信息提供方式(用户名、密码、当前用户的资源权限)
* 2、可采用内存存储方式,也可采用数据库方式等
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 用户信息存储在内存中,这样控制台就没有自动密码了
// String password = passwordEncoder().encode("1234");
// logger.info("加密之后存储的密码:{}", password);
// auth.inMemoryAuthentication()
// .withUser("mengxuegu") // 用户名
// .password(password) // 密码
// .authorities("ADMIN"); // 权限标识
auth.userDetailsService(customUserDetailsService);
}
/**
* 资源权限配置(过滤器链)
* 1、拦截的哪一些资源
* 2、资源所对应的角色权限
* 3、定义认证方式:httpBasic、httpForm
* 4、定制登录页面、登录请求地址、错误处理方式
* 5、自定义springSecurity过滤器等
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.httpBasic()
// http.formLogin() // 表单认证
// .loginPage("/login/page") // 交给/login/page 响应认证(登录)界面 (自定义登录界面)
// .loginProcessingUrl("/login/form") // 登录表单提交处理url,默认是/login
// .usernameParameter("name") // 默认用户名的属性名是username
// .passwordParameter("pwd") // 默认密码的属性名是password
// .and()
// .authorizeRequests() // 认证请求
// .antMatchers("/login/page").permitAll() // 放行跳转认证请求
// .anyRequest() // 所有进入应用的http请求都要进行认证
// .authenticated();
http.addFilterBefore(mobileValidateFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin() // 表单认证
.loginPage(securityProperties.getAuthentication().getLoginPage()) // 交给/login/page 响应认证(登录)界面 (自定义登录界面)
.loginProcessingUrl(securityProperties.getAuthentication().getLoginProcessingUrl()) // 登录表单提交处理url,默认是/login
.usernameParameter(securityProperties.getAuthentication().getUsernameParameter()) // 默认用户名的属性名是username
.passwordParameter(securityProperties.getAuthentication().getPasswordParameter()) // 默认密码的属性名是password
.successHandler(customAuthenticationSuccessHandler) // 认证成功处理器
.failureHandler(customAuthenticationFailureHandler) // 认证失败处理器
.and()
.authorizeRequests() // 认证请求
.antMatchers(securityProperties.getAuthentication().getLoginPage(), "/code/image", "/mobile/page", "/code/mobile").permitAll() // 放行跳转认证请求
.anyRequest() // 所有进入应用的http请求都要进行认证
.authenticated();
http.csrf().disable();// 关闭跨站请求伪造
// 将手机相关的配置绑定到过滤器链上
http.apply(mobileAuthenticationConfig);
}
/**
* 释放静态资源,核心过滤器配置方法
*
* @param web
*/
@Override
public void configure(WebSecurity web) {
// web.ignoring().antMatchers("/dist/**", "/modules/**", "/plugins/**");
web.ignoring().antMatchers(securityProperties.getAuthentication().getStaticPaths());
}
}
创建配置类ReloadMessageConfig,加载中文的认证提示信息到spring容器中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-URoLiIMT-1671776864272)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221016142700311.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t1Y80wyz-1671776864273)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221012160420191.png)]
可以通过抛出BadCredentialsException异常,来自定义异常信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vgndVwBK-1671776864274)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115110139564.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nPXXp8Up-1671776864274)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115110221224.png)]
1.重点关注UserDetailsService、UserDetails接口
2.自定义一个UserDetailsService接口的实现类CustomUserDetailsService,实现该接口中的loadUserByUsername 方法 ,通过该方法定义获取用户信息的逻辑
从数据库获取到的用户信息封装到UserDetais
接口的实现类中(Spring Security 提供了一个org.springframework.security.core.userdetails.User实现类封装用户信息)。
如果未获取到用户信息
,则抛出异常throws UsernameNotFoundException
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Suo4ihXc-1671776864275)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221016144048000.png)]
3.具体编码实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o50o0kIT-1671776864275)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221016144130529.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CS8eS4WN-1671776864276)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221016144241572.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SCSNDynB-1671776864276)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221016144602460.png)]
UsernamePasswordAuthenticationFilter#attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 从request中获取用户名密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 将username和password构造成一个UsernamePasswordAuthenticationToken实例,
// 其中构造器中会是否认证设置为authenticated=false
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
// 向authRequest对象中设置详细属性值。如添加了remoteAddress、sessionId值
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
ProviderManager#authenticate
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 获取当前的Authentication的认证类型
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
// 遍历认证提供者,不同认证方式有不同提供者,如:用户名密码认证提供者,手机短信认证提供者
for (AuthenticationProvider provider : getProviders()) {
// 选取当前认证方式对应的提供者
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 进行认证操作
result = provider.authenticate(authentication);
if (result != null) {
// 认证通过的话,将认证结果的details赋值到当前认证对象authentication,然后跳出循环
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
AbstractUserDetailsAuthenticationProvider#authenticate
AbstractUserDetailsAuthenticationProvider是 AuthenticationProvider 的核心实现类
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 如果authentication不是UsernamePasswordAuthenticationToken类型,则抛出异常
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
// 获取用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 从缓存中获取UserDetails
UserDetails user = this.userCache.getUserFromCache(username);
// 当缓存中没有UserDetails,则从字类DaoAuthenticationProvider中获取
if (user == null) {
cacheWasUsed = false;
try {
// 子类DaoAuthenticationProvider中实现获取用户信息,
// 就是调用UserDetailsService#loadUserByUsername
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 前置检查。DefaultPreAuthenticationChecks检查账户是否锁定、是否可用、是否过期等
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
// 异常则重新认证
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 后置检查,由DefaultPostAuthenticationCheck实现(检测密码是否过期)
postAuthenticationChecks.check(user);
// 是否放到缓存中
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 将认证成功用户信息封装成UsernamePasswordAuthenticationToken对象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
实现流程:
手机号登录是不需要密码的,通过短信验证码实现免密登录功能
1.向手机发送手机验证码,使用第三方短信平台 SDK 发送,如: 阿里云短信服务(阿里大于)
2.登录表单输入短信验证码
3.使用自定义过滤器MobileValidateFilter
4.当验证码校验通过后,进入自定义手机认证过滤器MobileAuthenticationFilter校验手机号是否存在
5.自定义MobileAuthenticationToken提供给MobileAuthenticationFilter
6.自定义MobileAuthenticationProvider提供给ProviderManager 处理
7.创建针对手机号查询用户信息的MobileUserDetailsService,交给 MobileAuthenticationProvider
8.自定义 MobileAuthenticationConfig 配置类将上面组件连接起来,添加到容器中
9.将 MobileAuthenticationConfig 添加到 SpringSecurityConfig 安全配置的过滤器链上。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1bMYVRa9-1671776864277)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221016150236445.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vnIOVnR1-1671776864277)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115142459604.png)]
2、OAuth2.0涉及的角色
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8SJoKAiK-1671776864277)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221013213349032.png)]
3、四种授权方式
1)、授权码模式:功能最完整,流程最严密的授权模式。国内各大服务提供商(微信、QQ、微 博、淘宝 、百度)都采用此模式进行授权。可以确定是用户真正同意授权;而且令牌是认证服务器发放给第三方应 用的服务器,而不是浏览器上。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K48No5Sf-1671776864278)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221013213245237.png)]
2)、简化模式(Implicit): 令牌是发放给浏览器的,oauth客户端运行在浏览器中 ,通过JS脚本去申请令牌。而不是发放 给第三方应用的服务器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AhVmfE4u-1671776864278)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221013213602500.png)]
**3)、密码模式(Resource Owner Password Credentials):**将用户名和密码传过去,直接获取 access_token 。用户 同意授权动作是在第三方应用上完成 ,而不是在认证服务器上。第三方应用申请令牌时,直接带着用户名密码去向 认证服务器申请令牌。这种方式认证服务器无法断定用户是否真的授权了,用户名密码可能是第三方应用盗取来的。
(重点关注,我们的系统就会用到此授权模式)
密码模式流程:
1.用户向客户端直接提供认证服务器平台的用户名和密码。
2.客户端将用户名和密码发给认证服务器,向后者请求令牌。
3.认证服务器确认无误后,向客户端提供访问令牌。
**4)、客户端证书模式(Client credentials):**用得少。当一个第三应用自己本身需要获取资源(而不是以用户的名 义),客户端模式十分有用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlEE2dlv-1671776864278)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221013214137595.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Pqtlt4T-1671776864279)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115205718296.png)]
1、创建认证服务器配置类
package com.mengxuegu.oauth2.server.config;
import com.mengxuegu.oauth2.server.service.CustomUserDetailsService;
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.crypto.password.PasswordEncoder;
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.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* 认证服务器配置
*
* @Author wangzw
* @Date 2022/10/13 14:54
*/
@Configuration
@EnableAuthorizationServer // 开始oauth2认证服务器功能
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* SpringSecurityConfig添加到容器中了
*/
@Autowired
private AuthenticationManager authenticationManager;
@Resource(name = "customUserDetailsService")
private CustomUserDetailsService customUserDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private DataSource dataSource;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
// 授权码管理策略
@Bean
public AuthorizationCodeServices jdbcAuthorizationCodeServices() {
// JDBC方式保存授权码到oauth_code表中
// 意义不大,因为获取一次令牌后,授权码就失效了
return new JdbcAuthorizationCodeServices(dataSource);
}
// jdbc管理客户端信息
@Bean
public ClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 配置被允许访问此认证服务器的客户端详情信息
* 方式1:内存方式管理
* 方式2:数据库管理
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用内存方式
// clients.inMemory()
// 允许访问此认证服务器的客户端id,如:PC,APP,小程序等,各自不同的客户端id
// .withClient("mengxuegu-pc") // 客户端id
// // 客户端密码,要加密,不然一直要求登录,获取不到令牌,而且一定不能被泄露
// .secret(passwordEncoder.encode("mengxuegu-secret"))
// // 资源id,如商品资源
// .resourceIds("product-server")
// // 授权类型,可同时支持多种授权类型
// .authorizedGrantTypes("authorization_code", "password", "implicit", "client_credentials", "refresh_token")
// // 授权范围标识,如指定微服务名称,则只能访问指定的微服务
// .scopes("all")
// // false跳转到授权页面手动点击授权,true不用手动授权,直接响应授权码
// .autoApprove(false)
// // 客户端回调地址
// .redirectUris("http://www.mengxuegu.com/");
// 使用JDBC管理客户端信息
clients.withClientDetails(jdbcClientDetailsService());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 密码模式要设置认证管理器
endpoints.authenticationManager(authenticationManager);
// 刷新令牌获取新令牌时需要
endpoints.userDetailsService(customUserDetailsService);
// 令牌管理策略(将令牌管理策略作用到认证服务器端点上)
// endpoints.tokenStore(tokenStore);
// jwt令牌管理策略
endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
// 授权码管理策略,针对授权码模式有效,会将授权码放到auth_code表,授权后就会被删除
endpoints.authorizationCodeServices(jdbcAuthorizationCodeServices());
}
/**
* 令牌端点的安全配置
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 所有人可访问/oauth/token_key后面要获取公钥,默认拒绝访问
security.tokenKeyAccess("permitAll()");
// 认证后可访问/oauth/check_token 默认拒绝访问
security.checkTokenAccess("isAuthenticated()");
}
}
2、安全配置类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6yj5SYcx-1671776864279)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115204211644.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qpnwOQh6-1671776864280)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221014152423906.png)]
默认情况下,令牌是通过 randomUUID 产生32位随机数的来进行填充的,而产生的令牌默认是存储在内存中。
内存存储采用的是 TokenStore 接口的默认实现类 InMemoryTokenStore , 开发时方便调试,适用单机版。
RedisTokenStore 将令牌存储到 Redis 非关系型数据库中,适用于并发高的服务。
JdbcTokenStore 基于 JDBC 将令牌存储到 关系型数据库中,可以在不同的服务器之间共享令牌。
JwtTokenStore (JSON Web Token)将用户信息直接编码到令牌中,这样后端可以不用存储它,前端拿到令
牌可以直接解析出用户信息。
配置步骤:
1)、创建TokenConfig令牌管理策略类
/**
* @Author wangzw
* @Date 2022/10/13 21:48
*/
@Configuration
public class TokenConfig {
/**
* redis管理令牌
* 1.启动redis服务器
* 2.添加redis相关依赖
* 3.添加redis依赖后,容器就会有RedisConnectionFactory实例
*/
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
@Bean
public TokenStore tokenStore() {
// Redis管理令牌
return new RedisTokenStore(redisConnectionFactory);
}
}
2)、令牌管理策略添加到端点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sGyQvpAw-1671776864280)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115211912236.png)]
3)、使用密码模式获取token
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3WvKpEDW-1671776864280)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115211953267.png)]
4)、查看redis管理工具
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NujxyFDi-1671776864281)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115212035619.png)]
配置方式:
1)、令牌管理策略类TokenConfig,使用JDBC方式去管理令牌
/**
* @Author wangzw
* @Date 2022/10/13 21:48
*/
@Configuration
public class TokenConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
@Bean
public TokenStore tokenStore() {
// JDBC管理令牌
return new JdbcTokenStore(dataSource());
}
}
2)、将令牌配置策略类应用到端点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cEPk2Cva-1671776864281)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115211912236.png)]
3)、测试用密码模式获取token,观察数据库表oauth_access_token会多出一条记录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TeJz4mZo-1671776864281)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115213502396.png)]
概述
资源服务器实际上就是对系统功能的增删改查,比如:商品管理、订单管理、积分管理、会员管理等资源,而在微
服务架构中,而这每个资源实际上就是每一个微服务。当用户请求某个微服务资源时,首先通过认证服务器进行认
证与授权,通过后再才可访问到对应资源。
实现的功能:
/**
* 资源服务器相关配置
*
* @Author wangzw
* @Date 2022/10/14 11:00
*/
@Configuration
@EnableResourceServer // 标识为资源服务器,所有发往这个服务的请求,都会去请求头里找token,找不到或者通过认证服务器验证不合法,则无法访问资源
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
// 配置当前资源服务器的ID
public static final String RESOURCE_ID = "product-server";
@Autowired
private TokenStore tokenStore;
/**
* 当前资源服务器的一些配置,如 资源服务器ID
*
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID) // 配置当前资源服务器的ID,会在认证服务器验证(客户端表resources配置了就可以访问这个服务)
// .tokenServices(tokenService()); // 实现令牌服务,ResourceServerTokenServices实例
.tokenStore(tokenStore);
}
// /**
// * 配置资源服务器如何验证token有效性
// * 1. DefaultTokenServices
// * 如果认证服务器和资源服务器同一服务时,则直接采用此默认服务验证即可
// * 2. RemoteTokenServices (当前采用这个)
// * 当认证服务器和资源服务器不是同一服务时, 要使用此服务去远程认证服务器验证
// * @return
// */
// @Bean
// public ResourceServerTokenServices tokenService() {
// // 资源服务器去远程认证服务器验证token是否有效
// RemoteTokenServices service = new RemoteTokenServices();
//
// // 请求认证服务器验证URL,注意:默认这个端点是拒绝访问的,要设置认证后可访问
// service.setCheckTokenEndpointUrl("http://localhost:8090/auth/oauth/check_token");
//
// // 在认证服务器配置的客户端id
// service.setClientId("mengxuegu-pc");
//
// // 在认证服务器配置的客户端密码
// service.setClientSecret("mengxuegu-secret");
//
// return service;
// }
/**
* 控制令牌范围权限和授权规则
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
// SpringSecurity不会创建也不会使用HttpSession
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 所有的请求对应访问的用户都要有all范围权限
.antMatchers("/**").access("#oauth2.hasScope('all')");
}
}
当认证服务器和资源服务器不是在同一工程时, 要使用 ResourceServerTokenServices 去远程请求认证服务器来校验
令牌的合法性,如果用户访问量较大时将会影响系统的性能
解决方式:
生成令牌采用 JWT 格式就可以解决上面的问题。
因为当用户认证后获取到一个JWT令牌,而这个 JWT 令牌包含了用户基本信息,客户端只需要携带JWT访问资源服
务器,资源服务器会通过事先约定好的算法进行解析出来,然后直接对 JWT 令牌校验,不需要每次远程请求认证服
务器完成授权。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CRzVImOL-1671776864282)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115215735286.png)]
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvZHVjdC1zZXJ2ZXIiXSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6WyJhbGwiLCJQUk9EVUNUX0FQSSJdLCJleHAiOjE2Njg1NzA2MzQsImF1dGhvcml0aWVzIjpbInN5czp1c2VyOmxpc3QiLCJwcm9kdWN0Omxpc3QiLCJzeXM6bWFuYWdlIiwic3lzOnVzZXIiLCJzeXM6cGVybWlzc2lvbjphZGQiLCJzeXM6aW5kZXgiLCJzeXM6dXNlcjphZGQiLCJzeXM6cGVybWlzc2lvbjplZGl0Iiwic3lzOnJvbGU6ZWRpdCIsInN5czpwZXJtaXNzaW9uIiwic3lzOnVzZXI6ZWRpdCIsInN5czp1c2VyOmRlbGV0ZSIsInN5czpyb2xlOmRlbGV0ZSIsInN5czpyb2xlOmxpc3QiLCJzeXM6cGVybWlzc2lvbjpkZWxldGUiLCJzeXM6cGVybWlzc2lvbjpsaXN0Iiwic3lzOnJvbGUiLCJzeXM6cm9sZTphZGQiXSwianRpIjoiYTg5YzBjYzQtNmI4Zi00ZjgzLTk4YzAtNDRmMWRmNmUyZGUwIiwiY2xpZW50X2lkIjoibWVuZ3h1ZWd1LXBjIn0.IwDTSQo0c8sJ10gHk53tSmT-Awug4mAnAknOm9K3Poks4RiTaVZRozVtQMXWv_Ni6WGZNPQhGyzHxrUcPOWvPU5F8cbRNxhKIa4gpdPtPhjTvgA55h-i789jVYSVE1NtH5VfJeD3UVrwCV3zuu4cbFYaXekRCR0WSv9qQiMuMzinKpH1q3jWPTqu7zZ_j32KMTId0E0WV_nSX5B2KkfCc2Kjz-gNjtp8pz4Vir8heXqcSomqAez4u_2T0diBmV0vRr_avTvhbj9rCwLXPmAuemRbepY3jNa8NKbssOK9dXM-tubbUwGYo1IfWt94INWJ2ZxYeFqURZCPRFtA1FBwNQ",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvZHVjdC1zZXJ2ZXIiXSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6WyJhbGwiLCJQUk9EVUNUX0FQSSJdLCJhdGkiOiJhODljMGNjNC02YjhmLTRmODMtOThjMC00NGYxZGY2ZTJkZTAiLCJleHAiOjE2NzExMTI2MzQsImF1dGhvcml0aWVzIjpbInN5czp1c2VyOmxpc3QiLCJwcm9kdWN0Omxpc3QiLCJzeXM6bWFuYWdlIiwic3lzOnVzZXIiLCJzeXM6cGVybWlzc2lvbjphZGQiLCJzeXM6aW5kZXgiLCJzeXM6dXNlcjphZGQiLCJzeXM6cGVybWlzc2lvbjplZGl0Iiwic3lzOnJvbGU6ZWRpdCIsInN5czpwZXJtaXNzaW9uIiwic3lzOnVzZXI6ZWRpdCIsInN5czp1c2VyOmRlbGV0ZSIsInN5czpyb2xlOmRlbGV0ZSIsInN5czpyb2xlOmxpc3QiLCJzeXM6cGVybWlzc2lvbjpkZWxldGUiLCJzeXM6cGVybWlzc2lvbjpsaXN0Iiwic3lzOnJvbGUiLCJzeXM6cm9sZTphZGQiXSwianRpIjoiYmZiODNkYmMtNzhhOS00MmY3LTg1ZTktMjNkYTA3NGRhNWNhIiwiY2xpZW50X2lkIjoibWVuZ3h1ZWd1LXBjIn0.iQZs8A-Oasys5za_QX2Y91m3I0fPOQpEbOAX6HJo8A-z4AwAUWVvI6sis_dj_rDioZocTjW_3ZIISMArMhT-0vOLmLyu_VP9ryIz_Fl60OM7GJ9JTPNl8Gic4RCYSG7cQIx9IYsNATCGEu_hNR0eswyMi_vyvaTKie4oerLk5r2JZeC2DquXqPmiVHOx5HjgRWOjpoW2nfEsekqcNHj8eSK372x5V3J3SxOEgsxYV5lSSEB_J1KbZ16vT-oAsadi9MpQzL7hoTEDdbF5urRYY6jLp4-NU-7A0ICHjHwslJKH7BnEv44LgYntQuqND8FX6rL_RItDakGEWWtLZBZ5fg",
"expires_in": 49999,
"scope": "all PRODUCT_API",
"jti": "a89c0cc4-6b8f-4f83-98c0-44f1df6e2de0"
}
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之
间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算
法)或使用RSA或ECDSA的公钥/私钥对进行签名 ,防止被篡改。
JWT 官网: https://jwt.io 想深入了解的可网站查看
JWT **的构成:
**JWT 有三部分构成:头部、有效载荷、签名
例如:aaaaa.bbbbbb.cccccccc
**头部:**包含令牌的类型(JWT) 与加密的签名算法((如 SHA256 或 ES256) ,Base64编码后加入第一部分
有效载荷:通俗一点讲就是token中需要携带的信息都将存于此部分,比如:用户id、权限标识等信息。
注:该部分信息任何人都可以读出来,所以添加的信息需要加密才会保证信息的安全性
**签名:**用于防止 JWT 内容被篡改, 会将头部和有效载荷分别进行 Base64编码,编码后用 . 连接组成新的字符
串,然后再使用头部声明的签名算法进行签名。在具有秘钥的情况下,可以验证JWT的准确性,是否被篡改
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bYQY7wil-1671776864282)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115153759370.png)]
配置:
1)、在认证服务器的TokenConfig管理策略类中
/**
* @Author wangzw
* @Date 2022/10/13 21:48
*/
@Configuration
public class TokenConfig {
/**
* 在JwtAccessTokenConverter中定义Jwt签名密码
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 读取oauth2.jks文件中的私钥,第二个参数是口令oauth2
KeyStoreKeyFactory keyFactory = new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "oauth2".toCharArray());
// 别名oauth2
converter.setKeyPair(keyFactory.getKeyPair("oauth2"));
return converter;
}
@Bean
public TokenStore tokenStore() {
// JWT管理令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
2)、将令牌配置策略应用到端点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gnRolf8d-1671776864282)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115221041407.png)]
3)、使用密码模式来获取token
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YQGLzWY5-1671776864283)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115221151894.png)]
4)、检查JWT令牌,包含了用户信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VVmSzQt6-1671776864283)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115221233145.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lw9tAnpS-1671776864283)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115154942521.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Id0nxsNi-1671776864284)(C:\Users\wangzw\AppData\Roaming\Typora\typora-user-images\image-20221115163650723.png)]
http://localhost:7001/auth/oauth/authorize?client_id=mengxuegu-pc&response_type=code
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 读取oauth2.jks文件中的私钥,第二个参数是口令oauth2
KeyStoreKeyFactory keyFactory = new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "oauth2".toCharArray());
// 别名oauth2
converter.setKeyPair(keyFactory.getKeyPair("oauth2"));
return converter;
}
@Bean
public TokenStore tokenStore() {
// JWT管理令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
**2)、将令牌配置策略应用到端点**
[外链图片转存中...(img-gnRolf8d-1671776864282)]
**3)、使用密码模式来获取token**
[外链图片转存中...(img-YQGLzWY5-1671776864283)]
**4)、检查JWT令牌,包含了用户信息**
[外链图片转存中...(img-VVmSzQt6-1671776864283)]
### 五、Spring Cloud OAuth2分布式认证授权
[外链图片转存中...(img-lw9tAnpS-1671776864283)]
[外链图片转存中...(img-Id0nxsNi-1671776864284)]
http://localhost:7001/auth/oauth/authorize?client_id=mengxuegu-pc&response_type=code