续 实现授权服务器
编写登录用配置类
上次课我们已经在sys模块中编写准备好了3个Rest接口
1.根据用户名获得用户对象
2.根据用户id获得所有权限
3.根据用户id获得所有角色
下面要在knows-auth模块编写登录配置类UserDetailsServiceImpl来实现根据用户名返回Spring-Security框架需要的UserDetails对象的方法以便支持登录验证
auth模块创建service包
包中创建UserDetailsServiceImpl类
和单体portal项目登录配置思路一致,但是用户相关的信息要修改为Ribbon调用,最终代码如下
// 当前登录配置类需要保存到Spring容器
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private RestTemplate restTemplate;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.根据用户名获得用户对象
String url="http://sys-service/v1/auth/user?username={1}";
User user=restTemplate
.getForObject(url , User.class , username);
// 2.判断查询出来的用户是否存在
if(user==null){
// 如果用户对象为空,抛出异常表示登录失败
//throw new ServiceException("用户名密码错误!");
throw new UsernameNotFoundException("用户名密码错误!");
}
// 3.根据用户id查询用户所有权限
url="http://sys-service/v1/auth/permissions?id={1}";
// Ribbon请求的控制器方法返回值为List时
// 要使用该List泛型类型的数组来接收
Permission[] permissions=restTemplate
.getForObject(url , Permission[].class , user.getId());
// 4.根据用户id查询用户所有角色
url="http://sys-service/v1/auth/roles?id={1}";
Role[] roles=restTemplate
.getForObject(url , Role[].class , user.getId());
// 5.将权限和角色保存在auth数组中
String[] auth=new String[permissions.length+roles.length];
// 分别遍历权限和角色数组,将权限和角色的名称保存在auth数组中
int i=0;
for(Permission p: permissions){
auth[i++]=p.getName();
}
for(Role r: roles){
auth[i++]=r.getName();
}
// 6.创建UserDetails对象
UserDetails details= org.springframework.security
.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(auth)
.accountLocked(user.getLocked()==1) //是否锁定(false表示不锁定)
.disabled(user.getEnabled()==0) // 是否可用 (false表示可用)
.build();
// 7.千万别忘了返回 details!!!
return details;
}
}
上面的代码是关键的登录配置
必须测试一下才能安心使用
auth模块的test文件夹下编写测试代码如下
@Resource
UserDetailsServiceImpl userDetailsService;
@Test
void contextLoads() {
UserDetails details=userDetailsService
.loadUserByUsername("st2");
System.out.println(details);
// Nacos和sys先启动,在运行测试!
}
运行结果可能是:
org.springframework.security.core.userdetails.User@1bdf1: Username: st2;
Password: [PROTECTED];
Enabled: true;
AccountNonExpired: true;
credentialsNonExpired: true;
AccountNonLocked: true;
Granted Authorities: /index.html,/question/create,/question/detail,/question/uploadMultipleFile,ROLE_STUDENT
要保证Nacos启动并且sys模块启动
运行能输出用户信息表示一切正常
授权服务器核心配置
上面所有编写的类和代码都是为了核心配置做准备的
核心配置就是再创建一个类,这个类要继承一个父类
重写这个父类的三个方法,这三个方法的作用分别是
1.配置授权服务器的各种参数
2.配置客户端对应的各种权限
3.配置客户端允许使用的功能
security包中创建该类
AuthorizationServer(授权服务器)
@Configuration
// 这个注解表示当前类是Oauth2标准下实现的授权服务器配置类
// 表示启动授权服务器相关功能
@EnableAuthorizationServer
public class AuthorizationServer extends
AuthorizationServerConfigurerAdapter {
// 添加依赖注入的对象,它们大多是之前课程中向Spring容器中保存准备好的配置
// Spring-Security框架的授权管理器,Oauth2要使用
@Resource
private AuthenticationManager authenticationManager;
// 要登录肯定需要登录配置类
@Resource
private UserDetailsServiceImpl userDetailsService;
// 核心配置方法1:配置授权服务器的各种参数
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// Oauth2框架提供了很多控制器方法
// 所以我们配置的就是这些控制器方法运行的内容
// endpoints(端点)参数其实就是控制器方法的功能
// 配置登录时的授权管理器
endpoints.authenticationManager(authenticationManager)
// 设置登录配置类
.userDetailsService(userDetailsService)
// 配置登录允许的提交方式
.allowedTokenEndpointRequestMethods(HttpMethod.POST)
// 配置令牌生成器对象
.tokenServices(tokenService());
}
// 注入保存令牌配置的对象
@Resource
private TokenStore tokenStore;
// 添加客户端详情对象(框架提供的对象,不是我们写的)
@Resource
private ClientDetailsService clientDetailsService;
// Spring容器保存令牌生成器对象
@Bean
public AuthorizationServerTokenServices tokenService(){
// 实例化 令牌生成器对象
DefaultTokenServices services=
new DefaultTokenServices();
// 设置令牌保存策略
services.setTokenStore(tokenStore);
// 设置令牌有效期(单位是秒,1800就是半小时)
services.setAccessTokenValiditySeconds(3600);
// 指定生成令牌的客户端,设置客户端详情
services.setClientDetailsService(clientDetailsService);
// 最后返回令牌生成器对象
return services;
}
// 获得加密对象,用于下面方法中的加密操作
@Resource
private PasswordEncoder passwordEncoder;
// 核心配置方法2:配置客户端对应的各种权限
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置当前授权服务器支持的所有客户端
// 以及客户端的权限
// 因为我们现在只有一个<<达内知道>>项目,所以只配置这一个客户端
// 客户端比较少可以直接保存在内存
// 如果客户端比较多,可能需要连接数据库获得权限信息
clients.inMemory() // 内存中保存客户端信息
.withClient("knows") //定义客户端名称
// 客户端定义的加密密码
.secret(passwordEncoder.encode("123456"))
// 配置<<达内知道项目的权限>>
.scopes("main")
// 配置<<达内知道>>登录方式
// 用户名密码登录方式只是Oauth2多种方式之一
// password这个单词不能写错
.authorizedGrantTypes("password");
}
// 核心配置方法3:配置客户端允许使用的功能
// 如果是一个大型的授权服务器,需要在这个方法中配置
// 哪些客户端允许运行什么功能,
// 我们的客户端是开放所有功能,所以配置也是比较简单的
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 允许哪些客户端生成令牌(配置permitAll全部允许)
.tokenKeyAccess("permitAll()")
// 允许哪些客户端验证令牌(配置permitAll全部允许)
.checkTokenAccess("permitAll()")
// 允许通过验证的客户端保存令牌
.allowFormAuthenticationForClients();
}
}
授权服务器生成令牌测试
保证要在运行\启动的组件有
Nacos
knows-sys
knows-auth
启动postman来测试
http://localhost:8010/oauth/token?client_id=knows&client_secret=123456&username=st2&password=888888&grant_type=password
别忘了把要登录的用户的密码列前面的{bcrypt}删除
删除后密码为
$2a 10 10 10ELGiEhKyLlO9r3.WVOkHDe16JTCKCErcABhElD5CF7ZwQ.Hm6sVRW
运行结果可能为:
{
"access_token": "e5f479ce-0bde-4192-8815-947c96d6f3b2",
"token_type": "bearer",
"expires_in": 1799,
"scope": "main"
}
授权服务器验证令牌测试
所谓令牌的验证就是根据令牌解析得到用户登录信息的操作
现在的令牌是一个uuid字符串
这个uuid是服务器内存中保存用户信息的key
我们可以使用这个key从服务器内存中获得用户信息
http://localhost:8010/oauth/check_token?token=[复制你的令牌]
其中token=之后的内容是上面章节中生成的令牌不要直接复制粘贴
验证解析过程是不限制get\post请求方式的
JWT令牌
上面提到了我们的用户信息仍然保存在服务器的内存中
这与我们当初设计的令牌性质不同
我们希望将包含用户信息的令牌转换为加密字符串响应给浏览器来保存
而不再使用服务器内存保存
这个问题的解决方案就是JWT
什么是JWT
JWT(Json Web Token)
它是一个通过json格式的信息,在网页和服务器传递过程中加密后保存到客户端的字符串
客户端持有当前用户信息的Jwt后,在访问需要表明自己身份的资源服务器时,将JWT连同请求一起发送给资源服务器,这个服务器就会解析令牌获得用户信息了
下面我们就开始将我们现在授权服务器中保存在内存中的用户信息,通过配置的修改转到JWT保存
配置生成JWT
knows-auth模块
修改TokenConfig类
// 只要是Spring的配置,就需要添加这个注解
@Configuration
public class TokenConfig {
// 配置保存令牌的策略对象到Spring容器
// 1.保存在内存中
// 2.生成令牌保存在客户端
// 保存为JWT令牌
// 定义解析JWT的口令
private final String SIGNING_KEY="knows-jwt";
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
// 定义一个Jwt令牌转换器对象(能够将json格式的用户信息转换为Jwt令牌)
JwtAccessTokenConverter converter=
new JwtAccessTokenConverter();
// 设置令牌解析口令
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
除此之外,还要修改核心配置类AuthorizationServer中tokenService方法中生成令牌的代码也要修改
@Resource
private JwtAccessTokenConverter accessTokenConverter;
// Spring容器保存令牌生成器对象
@Bean
public AuthorizationServerTokenServices tokenService(){
// 实例化 令牌生成器对象
DefaultTokenServices services=
new DefaultTokenServices();
// 设置令牌保存策略
services.setTokenStore(tokenStore);
// 新增代码 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 实例化一个令牌增强对象
TokenEnhancerChain chain=new TokenEnhancerChain();
// 将Jwt令牌转换器保存在令牌增强器中
chain.setTokenEnhancers(
Arrays.asList(accessTokenConverter));
// 将令牌增强器赋值给令牌生成器
services.setTokenEnhancer(chain);
// 新增代码结束 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// 设置令牌有效期(单位是秒,1800就是半小时)
services.setAccessTokenValiditySeconds(3600);
// 指定生成令牌的客户端,设置客户端详情
services.setClientDetailsService(clientDetailsService);
// 最后返回令牌生成器对象
return services;
}
重启auth模块
同时保证Nacos和sys模块正在运行
再去postman提交信息,观察结果
{
"access_token": "eyJhbGc.....pAnHFafE",
"token_type": "bearer",
"expires_in": 1799,
"scope": "main",
"jti": "df256c1a-91ef-4606-baca-2f40a36f91a4"
}
要想验证jwt
上面的请求应该能够得到包含jwt在内的响应结果
下面的请求将jwt复制到token后面
http://localhost:8010/oauth/check_token?token=[你的令牌粘贴在这]
解析得到包含用户信息的响应
实现页面登录
上面完成了授权服务器根据用户登录信息生成令牌的功能
下面我们要继续完成单点登录的其它内容
配置auth模块的网关路由
和sys\faq模块一样
auth授权服务器也要通过网关访问
转到gateway模块
application.yml添加路由信息
- id: gateway-auth
uri: lb://auth-service
predicates:
- Path=/oauth/**
启动gateway网关项目
再次测试生成和验证令牌,但是端口号由8010修改为9000
如果还能正常运行表示路由配置完成
开发登录页
转到knows-client前端项目
修改login.html页面中的代码
以支持访问授权服务器返回令牌
我们需要自己编写Vue和axios请求完成
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>达内知道登录</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.css">
<link rel="stylesheet" href="css/login.css" >
</head>
<body class="bg-light">
<div class="container-fluid">
<div class="row">
<div class="mx-auto mt-5" style="width: 400px;">
<h2 class="text-center "><b>达内</b>·知道</h2>
<!-- ↓↓↓↓↓↓↓ -->
<div class="bg-white p-4" id="loginApp">
<p class="text-center">用户登录</p>
<div id="error" class="alert alert-danger d-none">
<i class="fa fa-exclamation-triangle"></i> 账号或密码错误
</div>
<div id="logout" class="alert alert-info d-none">
<i class="fa fa-exclamation-triangle"></i> 已经登出系统
</div>
<div id="register" class="alert alert-info d-none">
<i class="fa fa-exclamation-triangle"></i> 已经成功注册,请登录。
</div>
<form action="/login" method="post"
@submit.prevent="login">
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
<div class="form-group has-icon">
<input type="text" class="form-control d-inline"
name="username" placeholder="手机号"
v-model="username">
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
<span class="fa fa-phone form-control-icon"></span>
</div>
<div class="form-group has-icon">
<input type="password" class="form-control"
name="password" placeholder="密码"
v-model="password">
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
<span class="fa fa-lock form-control-icon"></span>
</div>
<button type="submit" class="btn btn-primary btn-block ">登录</button>
</form>
<a class="d-block mt-1" href="resetpassword.html" >忘记密码?</a>
<a class="d-block mt-1" href="register.html" >新用户注册</a>
</div>
</div>
</div>
</div>
<script src="bower_components/jquery/dist/jquery.js" ></script>
<script src="bower_components/bootstrap/dist/js/bootstrap.js" ></script>
<!-- ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ -->
<script src="bower_components/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
if (location.search == "?error"){
$("#error").removeClass("d-none");
}
if (location.search == "?logout"){
$("#logout").removeClass("d-none");
}
if (location.search == "?register"){
$("#register").removeClass("d-none");
}
// ↓↓↓↓↓↓↓
let loginApp =new Vue({
el:"#loginApp",
data:{
username:"",
password:""
},
methods:{
login:function (){
let form=new FormData();
form.append("client_id","knows");
form.append("client_secret","123456");
form.append("grant_type","password");
form.append("username",this.username);
form.append("password",this.password);
axios({
url:"http://localhost:9000/oauth/token",
method:"post",
data:form
}).then(function(response){
alert(response.data.access_token);
})
}
}
})
</script>
</body>
</html>
启动knows-client项目
访问登录页,输入用户名和密码点击登录受阻
控制台显示跨域错误
Oauth2 授权服务器解决跨域问题
解决Oauth2授权服务器的跨域比较特殊
需要使用一个过滤器来解决,我们之前sys\faq模块的SpringMvc配置不能解决
Auth授权服务器跨域问题解决使用的过滤器时固定代码,所有Auth授权服务器都使用这个过滤器类来解决授权服务器的跨域问题
knows-auth模块
创建filter(过滤器)包
包中创建CorsFilter类,代码如下
import org.springframework.http.HttpHeaders;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CorsFilter implements Filter {
public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
public static final String OPTIONS = "OPTIONS";
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (isCorsRequest(httpRequest)) {
httpResponse.setHeader("Access-Control-Allow-Origin", "*");
httpResponse.setHeader("Access-Control-Allow-Methods",
"POST, GET, PUT, DELETE");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
// response.setIntHeader("Access-Control-Max-Age", 1728000);
httpResponse
.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Authorization");
if (isPreFlightRequest(httpRequest)) {
return;
}
}
chain.doFilter(request, response);
}
public void init(FilterConfig filterConfig) {
}
public void destroy() {
}
public boolean isCorsRequest(HttpServletRequest request) {
return (request.getHeader(HttpHeaders.ORIGIN) != null);
}
public boolean isPreFlightRequest(HttpServletRequest request) {
return (isCorsRequest(request) && OPTIONS.equals(request.getMethod()) && request
.getHeader(ACCESS_CONTROL_REQUEST_METHOD) != null);
}
}
过滤器的定义前面阶段讲解过
就是请求到达目标之前,先进行一些处理
这个过滤器的作用就是请求到达授权服务器的控制器方法之前
对请求中的跨域功能进行了设置,使得授权服务器的响应可以正常到达目标客户端
但是过滤器并不是定义了就立即生效的,我们需要在SpringBoot项目的配置中添加注册过滤器的代码,使其生效
auth模块任何带有@Configration注解的类都可以添加过滤器注册的配置,我们统一下SpringBoot启动类中配置即可
// 将授权服务器跨域过滤器注册到Springboot项目中
// 实现跨域效果,正常完成登录\验证等功能
@Bean
public FilterRegistrationBean registrationBean(){
// 实例化注册过滤器的对象
FilterRegistrationBean<CorsFilter> bean=
new FilterRegistrationBean<>();
// 设置过滤器生效的路径(过滤器都需要过滤哪些路径)
// 因为这个过滤器是设置跨域的,所有所有路径都需要过滤
bean.addUrlPatterns("/*");
// 设置当前过滤器优先级(当多个过滤器在同一个路径生效时,优先级高的先运行)
// 当前跨域过滤器优先级设置为最高
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
// 实例化过滤器对象
bean.setFilter(new CorsFilter());
return bean;
}
重启auth模块
Nacos\sys\gateway\client都要在启动状态
刷新login.html,输入用户信息后登录
观察是否能够获得alert弹出的jwt
将JWT保存在客户端
上面章节已经可以在js代码中得到JWT
下面就要将这个JWT保存在浏览器中
浏览器保存数据的方式有两种
- cookie:能够在浏览器中保存数据的功能,在之前阶段项目中使用过
- localStorage(本地仓库):也是浏览器提供的可以保存数据的功能
本次我们使用localStorage保存JWT
knows-client项目
login.html页面登录的axios方法的then中修改代码编写如下
.then(function(response){
//alert(response.data.access_token);
// 将登录成功时获得的JWT保存到LocalStorage
// localStorage使用很方便,类似一个Map结构
// setItem用于添加元素,getItem用户通过key获取元素
localStorage.setItem("accessToken",response.data.access_token);
// 登录成功,向首页跳转
location.href="/index.html";
}).catch(function(error){
// auth模块生成令牌失败会运行这个catch方法
// 可以输出方法参数error来查看报错信息
console.log(error);
})
重启knows-client项目
登录成功会跳转到index.html页面(虽然是404)
如果登录失败F12控制台会有报错信息
如果一切正常当前浏览器localStorage会保存当前JWT令牌
后面我们可以在需要时拿出表名自己身份
随笔
postman软件下载地址
https://www.postman.com/downloads/