赞
踩
认证功能几乎是每个项目都要具备的功能,并且它与业务无关,市面上有很多认证框架,如:Apache Shiro、CAS、Spring Security等。由于本项目基于Spring Cloud技术构建,Spring Security是spring家族的一份子且和Spring Cloud集成的很好,所以本项目选用Spring Security作为认证服务的技术框架。
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,它是一个专注于为 Java 应用程序提供身份验证和授权的框架。
项目主页:https://spring.io/projects/spring-security
Spring cloud Security: https://spring.io/projects/spring-cloud-security
项目包括学生、学习机构的老师、平台运营人员三类用户,三类用户将使用统一的认证入口,如下图:
用户输入账号和密码提交认证,认证通过继续操作。
项目由统一认证服务受理用户的认证请求,如下图:
认证通过由认证服务向给用户颁发令牌,相当于访问系统的通行证,用户拿着令牌去访问系统的资源。
如下图,用户只需要认证一次,便可以在多个拥有访问权限的系统中访问。
为了提高用户体验很多网站有扫码登录的功能,如:微信扫码登录、QQ扫码登录等。扫码登录的好处是用户不用输入账号和密码,操作简便,另外一个好处就是有利于用户信息的共享,互联网的优势就是资源共享,用户也是一种资源,对于一个新网站如果让用户去注册是很困难的,如果提供了微信扫码登录将省去用户的注册成本,是一种非常有效的推广手段。
微信扫码登录其中的原理正是使用了第三方认证,如下图:
首先先创建一个单独的模块
在yaml中新增下面配置:
server:
servlet:
context-path: /auth
port: 63070
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3307/xuecheng_users?serverTimezone=UTC&userUnicode=true&useSSL=false&
username: root
password: 1234
创建
controller
package com.xuecheng.auth.controller; /** * @author lemon * @version 1.0 * @description 测试controller * @date 2022/9/27 17:25 */ @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资源"; }
启动工程,尝试访问http://localhost:63070/auth/r/r1 :
访问用户信息:http://localhost:63070/auth/user/52
以上测试一切正常说明此工程部署成功。
下边向springboot工程集成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>
重启工程,访问http://localhost:63070/auth/r/r1
自动进入/login登录页面,/login是spring security提供的,此页面有几个css样式加载会稍微慢点,如下图:
账号和密码是多少呢?下一步需要进行安全配置。
代码如下:
package com.xuecheng.auth.config; /** * @author lemon * @version 1.0 * @description 安全管理配置 * @date 2022/9/26 20:53 */ @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(); } //配置安全拦截机制 @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过 .anyRequest().permitAll()//其它请求全部放行 .and() .formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success http.logout().logoutUrl("/logout");//退出地址 } }
重启工程
1、访问http://localhost:63070/auth/user/52 可以正常访问
2、访问http://localhost:63070/auth/r/r1 显示登录页面
账号zhangsan,密码为123,如果输入的密码不正确会认证失败,输入正确显示登录成功。
为什么http://localhost:63070/auth/user/52 可以正常访问,访问http://localhost:63070/auth/r/r1 显示登录页面?
http.logout().logoutUrl(“/logout”);配置了退出页面,认证成功后访问/logout可退出登录。
用户认证通过去访问系统资源时spring security进行授权控制,判断用户是否有该资源的访问权限,如果有则继续访问,如果没有则拒绝访问。
下边测试授权功能:
1、配置用户拥有哪些权限。
在WebSecurityConfig类配置zhangsan拥有p1权限,lisi拥有p2权限。
@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;
}
下边在controller
中配置/r/r1需要p1权限,/r/r2需要p2权限。
hasAuthority('p1')
表示拥有p1权限方可访问。
代码如下:
@RestController
public class LoginController {
....
@RequestMapping("/r/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
public String r1(){
return "访问r1资源";
}
@RequestMapping("/r/r2")
@PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问
public String r2(){
return "访问r2资源";
}
...
如果访问上不加@PreAuthorize
,此方法没有授权控制。
流程如下:
Spring Security的执行流程如下:
参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth协议:https://tools.ietf.org/html/rfc6749
下边分析一个Oauth2认证的例子,黑马程序员网站使用微信认证扫码登录的过程:
OAuth2
的几个授权模式是根据不同的应用场景以不同的方式去获取令牌,最终目的是要获取认证服务颁发的令牌,最终通过令牌去获取资源。
授权码模式简单理解是使用授权码去获取令牌,要想获取令牌先要获取授权码,授权码的获取需要资源拥有者亲自授权同意才可以获取。
下图是授权码模式交互图:
还以黑马网站微信扫码登录为例进行说明(认证服务就是自己写的auth工程):
1、用户打开浏览器。
2、通过浏览器访问客户端即黑马网站。
3、用户通过浏览器向认证服务请求授权,请求授权时会携带客户端的URL,此URL为下发授权码的重定向地址。
4、认证服务向资源拥有者返回授权页面。
5、资源拥有者亲自授权同意。
6、通过浏览器向认证服务发送授权同意。
7、认证服务向客户端地址重定向并携带授权码。
8、客户端即黑马网站收到授权码。
9、客户端携带授权码向认证服务申请令牌。
10、认证服务向客户端颁发令牌。
想测试授权模式首先要配置授权服务器即上图中的认证服务器,需要配置授权服务及令牌策略。
1、AuthorizationServer用 @EnableAuthorizationServer 注解标识并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权服务器。
package com.xuecheng.auth.config; /** * @author lemon * @version 1.0 * @description 授权服务器配置 * @date 2022/9/26 22:25 */ @Configuration @EnableAuthorizationServer public class AuthorizationServer extends AuthorizationServerConfigurerAdapter { // 注入令jwt令牌的生成 @Resource(name = "authorizationServerTokenServicesCustom") private AuthorizationServerTokenServices authorizationServerTokenServices; @Resource 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")//资源列表(资源服务必须要有这个资源id) .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.xuecheng-plus.com") ; } //令牌端点的访问配置 @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() //表单认证(申请令牌) ; } }
AuthorizationServerConfigurerAdapter
要求配置以下几个类:
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
public AuthorizationServerConfigurerAdapter() {}
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
}
1)ClientDetailsServiceConfigurer
:用来配置客户端详情服务(ClientDetailsService
),
随便一个客户端都可以随便接入到它的认证服务吗?答案是否定的,服务提供商会给批准接入的客户端一个身份,用于接入时的凭据,有客户端标识和客户端秘钥,在这里配置批准接入的客户端的详细信息。
2)AuthorizationServerEndpointsConfigurer
:用来配置令牌(token
)的访问端点和令牌服务(token services
)。
3)AuthorizationServerSecurityConfigurer
:用来配置令牌端点的安全约束.
ClientDetailsServiceConfigurer
就是相当于这个就是个OAuth2
的机制,别人从我这里得到令牌后就可以去访问我其他所有的微服务2、TokenConfig为令牌策略配置类
暂时先使用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、配置认证管理bean
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
....
重启认证服务
1、get请求获取授权码(肯定需要用户登录)
地址: /oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.xuecheng-plus.com
参数列表如下:
输入账号zhangsan
、密码123
登录成功,输入/oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.xuecheng-plus.com
显示授权页面
授权“XcWebApp”访问自己的受保护资源?
选择同意。
2、请求成功,重定向至http://www.xuecheng-plus.com/?code=授权码,比如:http://www.xuecheng-plus.com/?code=Wqjb5H
3、使用httpclient工具post申请令牌
/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=授权码&redirect_uri=http://www.xuecheng-plus.com/
参数列表如下
httpclient脚本如下:
### 授权码模式
### 第一步申请授权码(浏览器请求)/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.xuecheng-plus.com
### 第二步申请令牌
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=CTvCrB&redirect_uri=http://www.xuecheng-plus.com
申请令牌成功如下所示:
{
"access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2",
"token_type": "bearer",
"refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f",
"expires_in": 7199,
"scope": "all"
}
说明:
1、access_token,访问令牌,用于访问资源使用。
2、token_type,bearer是在RFC6750中定义的一种token类型,在携带令牌访问资源时需要在head中加入bearer 空格 令牌内容
3、refresh_token,当令牌快过期时使用刷新令牌可以再次生成令牌。
4、expires_in:过期时间(秒)
5、scope,令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权。
密码模式相对授权码模式简单,授权码模式需要借助浏览器供用户亲自授权,密码模式不用借助浏览器,如下图:
1、资源拥有者提供账号和密码
2、客户端向认证服务申请令牌,请求中携带账号和密码
3、认证服务校验账号和密码正确颁发令牌。
客户端申请到令牌,接下来客户端携带令牌去访问资源,到资源服务器将会校验令牌的合法性。
资源服务器如何校验令牌的合法性?
我们以OAuth2的密码模式为例进行说明:
这里存在一个问题:
就是校验令牌需要远程请求认证服务,客户端的每次访问都会远程校验,执行性能低。
jwt
就需要特定制作这样每一个资源服务就不用去远程请求认证服务,节省了问题的产生。
用户将令牌存储在客户端,去访问应用服务时携带令牌去访问,服务端从jwt解析出用户信息。这个过程就是无状态认证。
在认证服务中配置jwt令牌服务,即可实现生成jwt格式的令牌,
package com.xuecheng.auth.config; import java.util.Arrays; /** * @author Administrator * @version 1.0 **/ @Configuration public class TokenConfig { private String SIGNING_KEY = "mq123"; @Autowired TokenStore tokenStore; // @Bean // public TokenStore tokenStore() { // //使用内存存储令牌(普通令牌) // return new InMemoryTokenStore(); // } // 下面就是说传递的jwt就能解析出来token @Autowired private JwtAccessTokenConverter accessTokenConverter; @Bean public TokenStore tokenStore() { return new JwtTokenStore(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; } }
使用httpclient通过密码模式申请令牌
### 密码模式
POST {{auth_host}}/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123
生成jwt的示例如下:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzMzE2OTUsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6ImU5ZDNkMGZkLTI0Y2ItNDRjOC04YzEwLTI1NmIzNGY4ZGZjYyIsImNsaWVudF9pZCI6ImMxIn0.-9SKI-qUqKhKcs8Gb80Rascx-JxqsNZxxXoPo82d8SM",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJlOWQzZDBmZC0yNGNiLTQ0YzgtOGMxMC0yNTZiMzRmOGRmY2MiLCJleHAiOjE2NjQ1ODM2OTUsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6ImRjNTRjNTRkLTA0YTMtNDIzNS04MmY3LTFkOWZkMmFjM2VmNSIsImNsaWVudF9pZCI6ImMxIn0.Wsw1Jc-Kd_GFqEugzdfoSsMY6inC8OQsraA21WjWtT8",
"expires_in": 7199,
"scope": "all",
"jti": "e9d3d0fd-24cb-44c8-8c10-256b34f8dfcc"
}
客户端申请到jwt令牌,携带jwt去内容管理服务查询课程信息,此时内容管理服务要对jwt进行校验,只有jwt合法才可以继续访问。如下图:
1、在内容管理服务的content-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>
2、在内容管理服务的content-api工程中添加TokenConfig
package com.xuecheng.content.config; /** * @author Administrator * @version 1.0 **/ @Configuration public class TokenConfig { //jwt签名密钥,与认证服务保持一致 private String SIGNING_KEY = "mq123"; @Bean public TokenStore tokenStore() { //JWT令牌存储方案 return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(SIGNING_KEY); return converter; } /* @Bean public TokenStore tokenStore() { //使用内存存储令牌(普通令牌) return new InMemoryTokenStore(); }*/ }
3、添加资源服务配置
package com.xuecheng.content.config; /** * @author Administrator * @version 1.0 **/ @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) public class ResouceServerConfig extends ResourceServerConfigurerAdapter { // 资源服务标识 public static final String RESOURCE_ID = "xuecheng-plus"; @Autowired TokenStore tokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID) //资源 id .tokenStore(tokenStore) .stateless(true); } @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/r/**","/course/**").authenticated() //所有/r/**的请求必须认证通过 .anyRequest().permitAll() ; } }
根据配置可知/course/**
开头的接口需要认证通过。
1、访问根据课程id
查询课程接口
GET http://localhost:63040/content/course/2
返回:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
jwt令牌中记录了用户身份信息,当客户端携带jwt访问资源服务,资源服务验签通过后将前两部分的内容还原即可取出用户的身份信息,并将用户身份信息放在了SecurityContextHolder
上下文,SecurityContext
与当前线程进行绑定,方便获取用户身份。
@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable("courseId") Long courseId){
//取出当前用户身份
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println(principal);
return courseBaseInfoService.getCourseBaseInfo(courseId);
}
认证服务生成jwt
令牌将用户身份信息写入令牌,认证服务哪里获取用户身份。
目前还是将用户信息硬编码并暂放在内存中,如下:
@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;
}
为什么需要进行网关鉴权,可能大部分同学认为每个资源服务就可以可以进行认证,为什么还要多一步
其实:
最终:
1、网站白名单维护
针对不用认证的URL全部放行。
2、校验jwt的合法性。
除了白名单剩下的就是需要认证的请求,网关需要验证jwt的合法性,jwt合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。
1、在网关工程添加依赖
<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>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency>
header
里面有autheration
里面那个jwt
请求头,但是有些没有,但是说也可以通过cookie
里面获取,因此这个要写两种判断方法package com.xuecheng.gateway.config; /** * @author lemon * @version 1.0 * @description 网关认证过虑器(判断是否登录) * @date 2022/9/27 12:10 */ @Component @Slf4j public class GatewayAuthFilter implements GlobalFilter, Ordered { //白名单 private static List<String> whitelist = null; static { //加载白名单 try ( InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties"); ) { Properties properties = new Properties(); properties.load(resourceAsStream); Set<String> strings = properties.stringPropertyNames(); whitelist = new ArrayList<>(strings); } catch (Exception e) { log.error("加载/security-whitelist.properties出错:{}", e.getMessage()); e.printStackTrace(); } } // 也就是jwt的存储 @Autowired private TokenStore tokenStore; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String requestUrl = exchange.getRequest().getPath().value(); AntPathMatcher pathMatcher = new AntPathMatcher(); //白名单放行 for (String url : whitelist) { if (pathMatcher.match(url, requestUrl)) { return chain.filter(exchange); } } //检查token是否存在 String token = getToken(exchange); MultiValueMap<String, HttpCookie> cookies = exchange.getRequest().getCookies(); List<HttpCookie> jwt = cookies.get("jwt"); // System.out.println(jwt); HttpCookie httpCookie = jwt.get(0); // System.out.println(httpCookie); String value = httpCookie.getValue(); System.out.println(value); if (StringUtils.isBlank(token) && StringUtils.isBlank(value)) { // 响应流的编程 return buildReturnMono("没有认证", exchange); } //判断是否是有效的token OAuth2AccessToken oAuth2AccessToken; try { if (StringUtils.isBlank(token)) { oAuth2AccessToken = tokenStore.readAccessToken(value); } else { // 使用tokenconfig里面的进行判断 oAuth2AccessToken = tokenStore.readAccessToken(token); } boolean expired = oAuth2AccessToken.isExpired(); if (expired) { return buildReturnMono("认证令牌已过期", exchange); } return chain.filter(exchange); } catch (InvalidTokenException e) { log.info("认证令牌无效: {}", token); return buildReturnMono("认证令牌无效", exchange); } } /** * 获取token */ private String getToken(ServerWebExchange exchange) { String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StringUtils.isBlank(tokenStr)) { return null; } String token = tokenStr.split(" ")[1]; if (StringUtils.isBlank(token)) { return null; } return token; } private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); String jsonString = JSON.toJSONString(new RestErrorResponse(error)); byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bits); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } @Override public int getOrder() { return 0; } }
当token正确时可以正常访问资源服务,token验证失败返回token无效:
{
"errMessage": "认证令牌无效"
}
基于的认证流程在研究Spring Security
过程中已经测试通过,到目前为止用户认证流程如下:
认证所需要的用户信息存储在用户中心数据库,现在需要将认证服务连接数据库查询用户信息。
前边了解到Spring Security
工作原理时有一张执行流程图,如下图:
用户提交账号和密码由DaoAuthenticationProvider
调用UserDetailsService
的loadUserByUsername()
方法获取UserDetails
用户信息。
UserDetailsService
package com.xuecheng.ucenter.service.impl; /** * @author lemon * @version 1.0 * @description TODO * @date 2022/9/28 18:09 */ @Service public class UserServiceImpl implements UserDetailsService { @Autowired XcUserMapper xcUserMapper; /** * @description 根据账号查询用户信息 * @param s 账号 * @return org.springframework.security.core.userdetails.UserDetails * @author lemon * @date 2022/9/28 18:30 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username)); if(user==null){ //返回空表示用户不存在 return null; } //取出数据库存储的正确密码 String password =user.getPassword(); //用户权限,如果不加报Cannot pass a null GrantedAuthority collection String[] authorities= {"test"}; //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入 UserDetails userDetails = User.withUsername(user.getUsername()).password(password).authorities(authorities).build(); return userDetails; } }
写到这里我们需要清楚框架调用loadUserByUsername()方法拿到用户信息之后是如何执行的,见下图:
密码改为加密
@Bean
public PasswordEncoder passwordEncoder() {
// //密码为明文方式
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
修改数据库中的密码为Bcrypt
格式,并且记录明文密码,稍后申请令牌时需要。
由于修改密码编码方式还需要将客户端的密钥更改为Bcrypt
格式.
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()// 使用in-memory存储
.withClient("XcWebApp")// client_id
// .secret("secret")//客户端密钥
.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.xuecheng-plus.com")
;
}
修改UserServiceImpl
如下:
将
user
对象转json
package com.xuecheng.ucenter.service.impl; /** * @author lemon * @version 1.0 * @description TODO * @date 2022/9/28 18:09 */ @Service public class UserServiceImpl implements UserDetailsService { @Autowired XcUserMapper xcUserMapper; /** * @description 根据账号查询用户信息 * @param s 账号 * @return org.springframework.security.core.userdetails.UserDetails * @author lemon * @date 2022/9/28 18:30 */ @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s)); if(user==null){ //返回空表示用户不存在 return null; } //取出数据库存储的正确密码 String password =user.getPassword(); //用户权限,如果不加报Cannot pass a null GrantedAuthority collection String[] authorities = {"p1"}; //为了安全在令牌中不放密码 user.setPassword(null); //将user对象转json String userString = JSON.toJSONString(user); //创建UserDetails对象 UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build(); return userDetails; } }
下边编写一个工具类在各个微服务中去使用,获取当前登录用户的对象。
package com.xuecheng.content.util; /** * @author lemon * @version 1.0 * @description 获取当前用户身份工具类 * @date 2022/10/18 18:02 */ @Slf4j public class SecurityUtil { public static XcUser getUser() { try { Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principalObj instanceof String) { //取出用户身份信息 String principal = principalObj.toString(); //将json转成对象 XcUser user = JSON.parseObject(principal, XcUser.class); return user; } } catch (Exception e) { log.error("获取当前登录用户身份出错:{}", e.getMessage()); e.printStackTrace(); } return null; } }
下边在内容管理服务中测试此工具类,以查询课程信息接口为例:
下边在内容管理服务中测试此工具类,以查询课程信息接口为例:
@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable("courseId") Long courseId){
//取出当前用户身份
// Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SecurityUtil.XcUser user = SecurityUtil.getUser();
System.out.println(user);
return courseBaseInfoService.getCourseBaseInfo(courseId);
}
首先创建一个DTO
类表示认证的参数:
package com.xuecheng.ucenter.model.dto; /** * @author lemon * @version 1.0 * @description 认证用户请求参数 * @date 2022/9/29 10:56 */ @Data public class AuthParamsDto { private String username; //用户名 private String password; //域 用于扩展 private String cellphone;//手机号 private String checkcode;//验证码 private String checkcodekey;//验证码key private String authType; // 认证的类型 password:用户名密码模式类型 sms:短信模式类型 private Map<String, Object> payload = new HashMap<>();//附加数据,作为扩展,不同认证类型可拥有不同的附加数据。如认证类型为短信时包含smsKey : sms:3d21042d054548b08477142bbca95cfa; 所有情况下都包含clientId }
此时loadUserByUsername()
方法可以修改如下:里面就是我们AuthParamsDto
的json
字符串
package com.xuecheng.ucenter.service.impl; /** * @author lemon * @version 1.0 * @description 自定义UserDetailsService用来对接Spring Security * @date 2022/9/28 18:09 */ @Slf4j @Service public class UserServiceImpl implements UserDetailsService { @Autowired XcUserMapper xcUserMapper; /** * @description 查询用户信息组成用户身份信息 * @param s AuthParamsDto类型的json数据 * @return org.springframework.security.core.userdetails.UserDetails * @author lemon * @date 2022/9/28 18:30 */ @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { AuthParamsDto authParamsDto = null; try { //将认证参数转为AuthParamsDto类型 authParamsDto = JSON.parseObject(s, AuthParamsDto.class); } catch (Exception e) { log.info("认证请求不符合项目要求:{}",s); throw new RuntimeException("认证请求数据格式不对"); } //账号 String username = authParamsDto.getUsername(); XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username)); if(user==null){ //返回空表示用户不存在 return null; } //取出数据库存储的正确密码 String password =user.getPassword(); //用户权限,如果不加报Cannot pass a null GrantedAuthority collection String[] authorities = {"p1"}; //将user对象转json String userString = JSON.toJSONString(user); //创建UserDetails对象 UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build(); return userDetails; } }
重新定义DaoAuthenticationProviderCustom
,这样就可以不用使用框架默认的校验的方式
package com.xuecheng.auth.config; /** * @description 自定义DaoAuthenticationProvider * @author lemon * @date 2022/9/29 10:31 * @version 1.0 */ @Slf4j @Component public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider { @Autowired public void setUserDetailsService(UserDetailsService userDetailsService) { super.setUserDetailsService(userDetailsService); } // 屏蔽密码对比 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // 这是为空就是校验密码就是我们自定义 } }
修改WebSecurityConfig
类指定daoAuthenticationProviderCustom
@Autowired
DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;
// 改为自己的
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProviderCustom);
}
定义认证Service 接口
package com.xuecheng.ucenter.service; /** * @description 认证service * @author lemon * @date 2022/9/29 12:10 * @version 1.0 */ public interface AuthService { /** * @description 认证方法 * @param authParamsDto 认证参数 * @return com.xuecheng.ucenter.model.po.XcUser 用户信息 * @author lemon * @date 2022/9/29 12:11 */ XcUserExt execute(AuthParamsDto authParamsDto); }
loadUserByUsername()
修改如下:
@Slf4j @Service public class UserServiceImpl implements UserDetailsService { @Autowired XcUserMapper xcUserMapper; @Autowired ApplicationContext applicationContext; // 获取到所有容器 @Autowired AuthService authService; /** * @description 查询用户信息组成用户身份信息 * @param s AuthParamsDto类型的json数据 * @return org.springframework.security.core.userdetails.UserDetails * @author lemon * @date 2022/9/28 18:30 */ @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { AuthParamsDto authParamsDto = null; try { //将认证参数转为AuthParamsDto类型 authParamsDto = JSON.parseObject(s, AuthParamsDto.class); } catch (Exception e) { log.info("认证请求不符合项目要求:{}",s); throw new RuntimeException("认证请求数据格式不对"); } //开始认证 authService.execute(authParamsDto); .....
到此我们基于Spring Security
认证流程修改为如下:
上面定义了AuthService
认证接口,下边实现该接口实现账号密码认证
package com.xuecheng.ucenter.service.impl; /** * @description 账号密码认证 * @author lemon * @date 2022/9/29 12:12 * @version 1.0 */ @Service("password_authservice") public class PasswordAuthServiceImpl implements AuthService { @Autowired XcUserMapper xcUserMapper; @Autowired PasswordEncoder passwordEncoder; @Override public XcUserExt execute(AuthParamsDto authParamsDto) { //账号 String username = authParamsDto.getUsername(); XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username)); if(user==null){ //返回空表示用户不存在 throw new RuntimeException("账号不存在"); } XcUserExt xcUserExt = new XcUserExt(); BeanUtils.copyProperties(user,xcUserExt); //校验密码 //取出数据库存储的正确密码 String passwordDb =user.getPassword(); String passwordForm = authParamsDto.getPassword(); boolean matches = passwordEncoder.matches(passwordForm, passwordDb); if(!matches){ throw new RuntimeException("账号或密码错误"); } return xcUserExt; } }
修改UserServiceImpl类,根据认证方式使用不同的认证bean
package com.xuecheng.ucenter.service.impl; /** * @author lemon * @version 1.0 * @description 自定义UserDetailsService用来对接Spring Security * @date 2022/9/28 18:09 */ @Slf4j @Service public class UserServiceImpl implements UserDetailsService { @Autowired XcUserMapper xcUserMapper; @Autowired ApplicationContext applicationContext; // @Autowired // AuthService authService; /** * @description 查询用户信息组成用户身份信息 * @param s AuthParamsDto类型的json数据 * @return org.springframework.security.core.userdetails.UserDetails * @author lemon * @date 2022/9/28 18:30 */ @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { AuthParamsDto authParamsDto = null; try { //将认证参数转为AuthParamsDto类型 authParamsDto = JSON.parseObject(s, AuthParamsDto.class); } catch (Exception e) { log.info("认证请求不符合项目要求:{}",s); throw new RuntimeException("认证请求数据格式不对"); } //认证方法 String authType = authParamsDto.getAuthType(); AuthService authService = applicationContext.getBean(authType + "_authservice",AuthService.class); XcUserExt user = authService.execute(authParamsDto); return getUserPrincipal(user); } /** * @description 查询用户信息 * @param user 用户id,主键 * @return com.xuecheng.ucenter.model.po.XcUser 用户信息 * @author lemon * @date 2022/9/29 12:19 */ public UserDetails getUserPrincipal(XcUserExt user){ //用户权限,如果不加报Cannot pass a null GrantedAuthority collection String[] authorities = {"p1"}; String password = user.getPassword(); //为了安全在令牌中不放密码 user.setPassword(null); //将user对象转json String userString = JSON.toJSONString(user); //创建UserDetails对象 UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build(); return userDetails; } }
重启认证服务,测试申请令牌接口。
1、测试账号和密码都正确的情况是否可以申请令牌成功。
2、测试密码错误的情况。
3、测试账号不存在情况。
这个不是重点,就是这个功能得到了实现
完善PasswordAuthServiceImpl
package com.xuecheng.ucenter.service.impl; /** * @description 账号密码认证 * @author lemon * @date 2022/9/29 12:12 * @version 1.0 */ @Service("password_authservice") public class PasswordAuthServiceImpl implements AuthService { @Autowired XcUserMapper xcUserMapper; @Autowired PasswordEncoder passwordEncoder; @Autowired CheckCodeClient checkCodeClient; @Override public XcUser execute(AuthParamsDto authParamsDto) { //校验验证码 String checkcode = authParamsDto.getCheckcode(); String checkcodekey = authParamsDto.getCheckcodekey(); if(StringUtils.isBlank(checkcodekey) || StringUtils.isBlank(checkcode)){ throw new RuntimeException("验证码为空"); } Boolean verify = checkCodeClient.verify(checkcodekey, checkcode); if(!verify){ throw new RuntimeException("验证码输入错误"); } //账号 String username = authParamsDto.getUsername(); XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username)); if(user==null){ //返回空表示用户不存在 throw new RuntimeException("账号不存在"); } //校验密码 //取出数据库存储的正确密码 String passwordDb =user.getPassword(); String passwordForm = authParamsDto.getPassword(); boolean matches = passwordEncoder.matches(passwordForm, passwordDb); if(!matches){ throw new RuntimeException("账号或密码错误"); } return user; } }
流程:
这些是微信的api不做介绍
根据OAuth2
协议授权码流程,结合本项目自身特点,分析接入微信扫码登录的流程如下:
通过 code
获取access_token
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
获取access_token后,进行接口调用,有以下前提:
access_token有效且未超时;
微信用户已授权给第三方应用帐号相应接口作用域(scope)。
package com.xuecheng.auth.controller; @Slf4j @Controller public class WxLoginController { @Resource private WxAuthServiceImpl wxAuthService; // 微信授权成功后的回调 @RequestMapping("/wxLogin") public String wxLogin(String code, String state) throws IOException { log.debug("微信扫码回调,code:{},state:{}", code, state); // 那授权码去查询用户 XcUser xcUser = wxAuthService.wxAuth(code); if (xcUser == null) { return "redirect:http://www.xuecheng-plus.com/error.html"; } // 自动登录 String username = xcUser.getUsername(); // 就去请求我们认证服务的方法(自动登录) return "redirect:http://www.xuecheng-plus.com/sign.html?username=" + username + "&authType=wx"; } }
定义微信认证的service
package com.xuecheng.ucenter.service.impl; /** * @author lemon * @version 1.0 * @description 微信扫码认证 * @date 2022/9/29 12:12 */ @Slf4j @Service("wx_authservice") public class WxAuthServiceImpl implements AuthService { @Autowired XcUserMapper xcUserMapper; public XcUser wxAuth(String code){ //获取access_token //获取用户信息 //添加用户到数据库 XcUser xcUser = null; return xcUser; } //微信扫码认证,不校验验证码,不校验密码 @Override public XcUserExt execute(AuthParamsDto authParamsDto) { //账号 String username = authParamsDto.getUsername(); XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username)); if (user == null) { //返回空表示用户不存在 throw new RuntimeException("账号不存在"); } XcUserExt xcUserExt = new XcUserExt(); BeanUtils.copyProperties(user, xcUserExt); return xcUserExt; } }
access_token
后public XcUser wxAuth(String code){ //收到code调用微信接口申请access_token Map<String, String> access_token_map = getAccess_token(code); if(access_token_map==null){ return null; } System.out.println(access_token_map); String openid = access_token_map.get("openid"); String access_token = access_token_map.get("access_token"); //拿access_token查询用户信息 Map<String, String> userinfo = getUserinfo(access_token, openid); if(userinfo==null){ return null; } //添加用户到数据库 XcUser xcUser = null; return xcUser; }
添加用户到数据库(回显判断是否存在)
@Transactional public XcUser addWxUser(Map userInfo_map){ String unionid = userInfo_map.get("unionid").toString(); //根据unionid查询数据库 XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getWxUnionid, unionid)); if(xcUser!=null){ return xcUser; } String userId = UUID.randomUUID().toString(); xcUser = new XcUser(); xcUser.setId(userId); xcUser.setWxUnionid(unionid); //记录从微信得到的昵称 xcUser.setNickname(userInfo_map.get("nickname").toString()); xcUser.setUserpic(userInfo_map.get("headimgurl").toString()); xcUser.setName(userInfo_map.get("nickname").toString()); xcUser.setUsername(unionid); xcUser.setPassword(unionid); xcUser.setUtype("101001");//学生类型 xcUser.setStatus("1");//用户状态 xcUser.setCreateTime(LocalDateTime.now()); xcUserMapper.insert(xcUser); XcUserRole xcUserRole = new XcUserRole(); xcUserRole.setId(UUID.randomUUID().toString()); xcUserRole.setUserId(userId); xcUserRole.setRoleId("17");//学生角色 xcUserRoleMapper.insert(xcUserRole); return xcUser; }
调用保存用户信息
public XcUser wxAuth(String code){ //收到code调用微信接口申请access_token Map<String, String> access_token_map = getAccess_token(code); if(access_token_map==null){ return null; } System.out.println(access_token_map); String openid = access_token_map.get("openid"); String access_token = access_token_map.get("access_token"); //拿access_token查询用户信息 Map<String, String> userinfo = getUserinfo(access_token, openid); if(userinfo==null){ return null; } //将用户信息保存到数据库 XcUser xcUser = currentProxy.addWxUser(userinfo); return xcUser; }
// 自动登录
String username = xcUser.getUsername();
// 就去请求我们认证服务的方法(自动登录)
return "redirect:http://www.xuecheng-plus.com/sign.html?username=" + username + "&authType=wx";
就是看这个用户是否有权限去操作这个接口
在需要授权的接口处使用@PreAuthorize("hasAuthority('权限标识符')")
进行控制
下边代码指定/course/list接口需要拥有xc_teachmanager_course_list 权限。
设置了@PreAuthorize表示执行此方法需要授权,如果当前用户请求接口没有权限则抛出异常
org.springframework.security.access.AccessDeniedException: 不允许访问
首先要学习数据模型,本项目授权相关的数据表如下:
说明如下:
xc_user:用户表,存储了系统用户信息,用户类型包括:学生、老师、管理员等
xc_role:角色表,存储了系统的角色信息,学生、老师、教学管理员、系统管理员等。
xc_user_role:用户角色表,一个用户可拥有多个角色,一个角色可被多个用户所拥有
xc_menu:模块表,记录了菜单及菜单下的权限
xc_permission:角色权限表,一个角色可拥有多个权限,一个权限可被多个角色所拥有
使用Spring Security
进行授权,首先在生成jwt前会查询用户的权限,如下图:
接下来需要修改UserServiceImpl
和PasswordAuthServiceImpl
从数据库查询用户的权限。
1、定义mapper接口
public interface XcMenuMapper extends BaseMapper<XcMenu> {
@Select("SELECT * FROM xc_menu WHERE id IN (SELECT menu_id FROM xc_permission WHERE role_id IN ( SELECT role_id FROM xc_user_role WHERE user_id = #{userId} ))")
List<XcMenu> selectPermissionByUserId(@Param("userId") String userId);
}
2、修改PasswordAuthServiceImpl
1)首先在XcUserExt
类中新增用户权限
@Data
public class XcUserExt extends XcUser {
//用户权限
List<String> permissions = new ArrayList<>();
}
2)修改UserServiceImpl
类的getUserPrincipal
方法,查询权限信息
//查询用户身份 public UserDetails getUserPrincipal(XcUserExt user){ String password = user.getPassword(); //查询用户权限 List<XcMenu> xcMenus = menuMapper.selectPermissionByUserId(user.getId()); List<String> permissions = new ArrayList<>(); if(xcMenus.size()<=0){ //用户权限,如果不加则报Cannot pass a null GrantedAuthority collection permissions.add("p1"); }else{ xcMenus.forEach(menu->{ permissions.add(menu.getCode()); }); } //将用户权限放在XcUserExt中 user.setPermissions(permissions); //为了安全在令牌中不放密码 user.setPassword(null); //将user对象转json String userString = JSON.toJSONString(user); String[] authorities = permissions.toArray(new String[0]); UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build(); return userDetails; }
权限这部分和我写的SpringSecurity
写的是差不多的,可以参考这里就简写了
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。