6.1 认证授权模块
一、需求分析
认证授权模块实现平台所有用户的身份认证与用户授权功能
1.1 认证授权
- 什么是用户身份认证?
用户身份认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。
常见的用户身份认证的表现形式有:用户名密码登录,微信扫码等方式
项目包括学生、学习机构的老师、平台运营人员三类用户,不管哪一类用户在访问项目受保护资源时都需要进行身份认证。
比如:
发布课程操作,需要学习机构的老师首先登录系统成功,然后再执行发布课程操作。
创建订单,需要学生用户首先登录系统,才可以创建订单
- 什么是用户授权?
用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。
比如:
用户去发布课程,系统首先进行用户身份认证,认证通过后继续判断用户是否有发布课程的权限,如果没有权限则拒绝继续访问系统,如果有权限则继续发布课程
流程是 用户认证 - 检查用户是否有操作权限 - 执行某个操作
1.2 统一认证 - 登录
统一的认证入口
用户输入账号和密码提交认证,认证通过则继续操作
认证通过由认证服务向给用户颁发令牌,相当于访问系统的通行证,用户拿着令牌去访问系统的资源
认证请求如下图所示
1.3 单点登录
单点登录:简称为 SSO(Single Sign On),用户只需要登录一次就可以访问所有相互信任的应用系统。
内容管理服务、媒资管理服务、学习中心服务、系统管理服务等,为了提高用户体验性,用户只需要认证一次便可以在多个拥有访问权限的系统中访问
1.4 第三方认证
为了提高用户体验,很多网站有扫码登录的功能,如:微信扫码登录、QQ扫码登录等。
扫码登录的好处是用户不用输入账号和密码,操作简便,另外一个好处就是有利于用户信息的共享,互联网的优势就是资源共享,用户也是一种资源,对于一个新网站如果让用户去注册是很困难的,如果提供了微信扫码登录将省去用户注册的成本,是一种非常有效的推广手段。
微信扫码登录其中的原理正是使用了第三方认证,如下图
二、Spring Security
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,它是一个专注于为 Java 应用程序提供身份验证和授权的框架。
项目主页:https://spring.io/projects/spring-security
Spring cloud Security: https://spring.io/projects/spring-cloud-security
2.1 创建 xuecheng-auth 工程
2.1.1 bootstrap.xml
spring:
application:
name: auth-service
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
config:
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
shared-configs:
- data-id: swagger-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: logging-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
profiles:
active: dev
nacos中auth-service-dev.yaml配置
server:
servlet:
context-path: /auth
port: 63070
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.65:3306/xcplus_users?serverTimezone=UTC&userUnicode=true&useSSL=false&
username: root
password: mysql
2.1.2 测试 - LoginController
@Slf4j
@RestController
public class LoginController {
@Autowired
XcUserMapper userMapper;
@RequestMapping("/login-success")
public String loginSuccess() {
return "登录成功";
}
@RequestMapping("/user/{id}")
public XcUser getuser(@PathVariable("id") String id) {
XcUser xcUser = userMapper.selectById(id);
return xcUser;
}
@RequestMapping("/r/r1")
public String r1() {
return "访问r1资源";
}
@RequestMapping("/r/r2")
public String r2() {
return "访问r2资源";
}
}
2.1.3 Maven 坐标
向auth认证工程集成Spring security,向pom.xml加入Spring Security所需要的依赖
<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>
启动auth项目,访问http://localhost:63070/auth/login
出现如下图所示的页面
2.1.4 安全管理配置 - WebSecurityConfig
/**
* @description 安全管理配置
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
//这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
// //密码为明文方式
return NoOpPasswordEncoder.getInstance();
// return new BCryptPasswordEncoder();
}
//配置安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
.anyRequest().permitAll()//其它请求全部放行
.and()
.formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
}
}
2.2 工作原理
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。
根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理
当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理
AuthenticationManager 用于认证
AccessDecisionManager 用于授权
下图是Spring Security过虑器链结构图
spring Security功能的实现主要是由一系列过滤器链相互配合完成
- SecurityContextPersistenceFilter
这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
- UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
- FilterSecurityInterceptor
是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
- ExceptionTranslationFilter
能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
Spring Security的执行流程如下
-
用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
-
然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
-
认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
-
SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
-
可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List<AuthenticationProvider>列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication
三、OAuth2
第三方认证
Oauth2是一个标准的开放的授权协议,应用程序可以根据自己的要求去使用Oauth2,本项目使用Oauth2实现如下目标:
1、学成在线访问第三方系统的资源
本项目要接入微信扫码登录所以本项目要使用OAuth2协议访问微信中的用户信息。
也就是要通过第三方获取用户信息
2、外部系统访问学成在线的资源
同样当第三方系统想要访问学成在线网站的资源也可以基于OAuth2协议,提高安全性。
别人的系统来访问我们的系统,我们也可以使用OAuth2,也就是我们系统要共享我们的数据,让别人可以访问
3、学成在线前端(客户端) 访问学成在线微服务的资源
本项目是前后端分离架构,前端访问微服务资源也可以基于OAuth2协议进行认证。
前端访问后端时要进行认证,不认证的话某些服务可能限制访问
3.1 介绍
微信扫码认证,这是一种第三方认证的方式,这种认证方式是基于OAuth2协议实现
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。
业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。
互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。
参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth协议:https://tools.ietf.org/html/rfc6749
3.2 微信扫码认证登录流程
- 用户点击微信扫码
用户进入登录页面,点击微信的图标开打微信扫码界面
微信扫码的目的是通过微信认证登录官网,网站需要从微信获取当前用户的身份信息才会让当前用户在网站登录成功
首先我们的网站没有用户的信息,我们需要从微信获取信息,但是从微信获取信息的时候需要用户进行同意,如果用户同意的了话,微信才会把用户的信息发送给我们的网站服务,我们网站服务拿到用户信息后,进行用户验证
资源:用户信息,在微信中存储。
资源拥有者:用户是用户信息资源的拥有者。
(只用用户同意后,微信才会把用户信息给到网站)
认证服务:微信负责认证当前用户的身份,负责为客户端颁发令牌。
客户端:客户端会携带令牌请求微信获取用户信息,网站即客户端,网站需要在浏览器打开。
- 用户授权黑马网站访问用户信息
资源拥有者扫描二维码表示资源拥有者请求微信进行认证,微信认证通过向用户手机返回授权页面,如下所示
询问用户是否授权访问自己在微信的用户信息,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码给我们的网站服务
只有资源拥有者同意微信才允许网站服务访问用户资源
-
网站服务获取到授权码
-
携带授权码请求微信认证服务器申请令牌
此交互过程用户看不到
- 微信认证服务器向网站服务响应令牌
此交互过程用户看不到。
- 网站请求微信资源服务器获取资源即用户信息
网站携带令牌请求访问微信服务器获取用户的基本信息
-
微信资源服务器返回受保护资源即用户信息
-
网站接收到用户信息,此时用户在黑马网站登录成功
3.3 Oauth2 认证流程
Oauth2包括以下角色
- 客户端
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:手机客户端、浏览器等。
上边示例中网站即为客户端,它需要通过浏览器打开。
- 资源拥有者
通常为用户,也可以是应用程序,即该资源的拥有者。
A表示 客户端请求资源拥有者授权
B表示 资源拥有者授权客户端即网站访问自己的用户信息
- 授权服务器(也称认证服务器)
认证服务器对资源拥有者进行认证,还会对客户端进行认证并颁发令牌。
C 客户端即网站携带授权码请求认证
D 认证通过颁发令牌
- 资源服务器
存储资源的服务器。
E表示客户端即黑马网站携带令牌请求资源服务器获取资源
F表示资源服务器校验令牌通过后提供受保护资源
3.4 OAuth2 授权模式
本项目使用授权码模式完成微信扫码认证。
本项目采用密码模式作为前端请求微服务的认证方式
Spring Security支持OAuth2认证
OAuth2提供授权码模式、密码模式、简化模式、客户端模式等四种授权模式,
前边举的微信扫码登录的例子就是基于授权码模式
这四种模式中授权码模式和密码模式应用较多,本节使用Spring Security演示授权码模式、密码模式
OAuth2的几个授权模式是根据不同的应用场景以不同的方式去获取令牌
最终目的是要获取认证服务颁发的令牌,最终通过令牌去获取资源
3.4.1 授权码模式
授权码模式简单理解是使用授权码去获取令牌,要想获取令牌先要获取授权码,授权码的获取需要资源拥有者(一般是用户)亲自授权同意才可以获取
流程
1、用户打开浏览器
2、通过浏览器访问客户端即网站
3、用户通过浏览器向认证服务请求授权,请求授权时会携带客户端的URL,此URL为下发授权码的重定向地址
4、认证服务向资源拥有者返回授权页面
5、资源拥有者亲自授权同意
6、通过浏览器向认证服务发送授权同意
7、认证服务向客户端地址重定向并携带授权码
8、客户端即网站收到授权码
9、客户端携带授权码向认证服务申请令牌
10、认证服务向客户端颁发令牌
3.4.2 授权码模式 - 测试
3.4.2.1 TokenConfig 令牌配置类
@Configuration
public class TokenConfig {
@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;
}
}
3.4.2.2 AuthorizationServer 授权服务器配置
AuthorizationServer用 @EnableAuthorizationServer 注解标识并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权服务器
1)ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),
随便一个客户端都可以随便接入到它的认证服务吗?答案是否定的,服务提供商会给批准接入的客户端一个身份,用于接入时的凭据,有客户端标识和客户端秘钥,在这里配置批准接入的客户端的详细信息。
2)AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
3)AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束
/**
* @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("XcWebApp")//客户端密钥
// .secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥
.resourceIds("xuecheng-plus")//资源列表
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允许的授权范围
.autoApprove(false)//false跳转到授权页面
//客户端接收授权码的重定向地址
.redirectUris("http://www.51xuecheng.cn")
;
}
//令牌端点的访问配置
@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() //表单认证(申请令牌)
;
}
}
3.4.2.3 WebSecurityConfig 配置认证管理bean
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
3.4.3 密码模式
1、资源拥有者提供账号和密码
2、客户端向认证服务申请令牌,请求中携带账号和密码
3、认证服务校验账号和密码正确颁发令牌。