实现登录首页
显示index.html
上次课结束时,我们已经将jwt令牌保存到了localStorage中
登录成功会转到index.html页面,但是因为index.html没有创建所有报404错误
下面就创建index.html避免404错误
knows-client项目
index.html创建代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
正在加载...请稍候
</body>
</html>
我们可以启动相关服务,来登录测试,
登录成功时会访问这个页面而不再报404
Nacos\gateway\sys\auth\client
index.html异步请求身份
下面我们就应该在index.html页面加载完毕时
向控制器发送一个确认身份的请求
确定当前登录用户的JWT令牌中的用户信息是学生还是讲师的身份
再根据这个身份跳转对应的首页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
正在加载...请稍候
</body>
<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>
<!--引入CDN服务器的框架文件-->
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script>
// 利用jQuery提供的页面加载完毕运行的方法,运行代码
$(function(){
// 获得localStorage中的jwt
let token=localStorage.getItem("accessToken");
// 将jwt token 输出到控制台
console.log(token);
})
</script>
</html>
创建根据不同身份跳转页面的控制器
index.html页面已经确定能够获得jwt
具备了向控制器方法发送包含jwt的请求的条件
下面我们就编写一个控制器方法来接收这个请求,根据不同身份跳转不同页面
portal项目中我们有HomeController离开判断登录用户身份
这里我们可以复制它,但是要进行响应修改
判断用户身份的业务我们编写在sys模块
转到knows-sys模块将HomeController复制
HomeController修改代码如下
// ↓↓↓↓↓↓↓↓↓↓
@RestController
// ↓↓↓↓↓↓↓↓↓↓
@RequestMapping("/v1/home")
public class HomeController {
// Spring-Security框架中角色\权限是框架设置好的类型
// 要判断具体的某个角色,建议将这个角色声明为常量类型,判断时使用
public static final GrantedAuthority STUDENT=
new SimpleGrantedAuthority("ROLE_STUDENT");
public static final GrantedAuthority TEACHER=
new SimpleGrantedAuthority("ROLE_TEACHER");
// localhost:9000/v1/home
// ↓↓↓↓↓↓↓↓↓↓
@GetMapping
public String index(
@AuthenticationPrincipal UserDetails user){
// 判断当前登录用户是否包含讲师角色
if(user.getAuthorities().contains(TEACHER)){
// 如果包含讲师角色,跳转到讲师首页
// ↓↓↓↓↓↓↓↓↓↓
return "/index_teacher.html";
}else if(user.getAuthorities().contains(STUDENT)){
// 如果不包含讲师角色,包含学生角色,跳转到学生首页
// ↓↓↓↓↓↓↓↓↓↓
return "/index_student.html";
}
// 既不是讲师也不是学生的用户暂不考虑,直接返回null
return null;
}
}
判断用户身份来跳转页面的控制器方法准备好了
但是有同一个非常重要的问题
这个控制器方法仍然在使用@AuthenticationPrincipal UserDetails user
来获得当前登录用户信息,而它获得用户信息的途径是在Spring-Security框架中寻找
我们现在使用了Jwt保存用户信息这个信息在客户端,
我们现在必须将Jwt解析为用户信息,再把用户信息保存到Spring-Security框架中,才能使上面的注解生效,控制器方法才能正常获得用户信息
综上所述,我们需要下面功能的代码
1.获得浏览器中的随请求发来的Jwt
2.将Jwt解析为用户信息(主要是用户名和所有权限\角色)
3.我们需要将这个用户信息使用Spring-Security框架指定的方式,保存到Spring-Security容器中,以便@AuthenticationPrincipal注解来获得
Spring MVC 拦截器
什么是拦截器
拦截器是SpringMvc框架提供的功能
它可以在控制器方法运行之前或运行之后(还有其它特殊时机)对请求进行处理或加工的特定接口
常见面试题:过滤器和拦截器的区别
提供者不同:
- 过滤器时javaEE提供的
- 拦截器是SpringMvc提供的
作用目标不同
- 过滤器作用目标比较广:可以作用在所有请求当前服务器资源的流程中
- 拦截器作用目标比较单一:只能作用在请求目标是控制器方法的流程中
功能强度不同:
- 过滤器是原生的JavaEE的功能,功能较弱,不能直接处理Spring容器中的内容存和对象
- 拦截器是SpringMvc框架提供的,所以和Spring框架的兼容性更好,可以直接使用操作Spring容器中的对象和内容,拦截器提供更多的运行时机,程序员可以选择更合适的来使用
总结
如果请求的目标能确定是一个控制器方法,那么优先选择拦截器
如果请求的目标可能是控制器或其他静态资源,那么需要使用过滤器
拦截器工作流程图
拦截器基本使用
使用拦截器的步骤
1.定义拦截器(创建一个实现指定拦截器接口的类)
2.配置拦截器(SpringMvc配置类中配置拦截器)
knows-sys模块编写测试拦截器的功能
创建一个拦截器类的包:interceptor
包中创建DemoInterceptor类,代码如下
// 拦截器对象也要保存到Spring容器统一管理
@Component
public class DemoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 这个方法会在控制器运行之前运行
System.out.println("preHandle运行");
// 该方法返回boolean类型
// 返回true表示允许当前请求继续访问控制器方法
// 返回false表示阻止当前请求继续访问控制器方法
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 在控制器方法运行之后执行
System.out.println("postHandle运行");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 在页面显示结果之前运行
System.out.println("afterCompletion运行");
}
}
定义拦截器之后要配置拦截器
配置拦截器的代码编写在SpringMvc配置类中,也就是WebConfig中即可
代码如下
// 配置拦截器,先从Spring容器中获得它
@Resource
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 先指定配置哪个拦截器对象
registry.addInterceptor(demoInterceptor)
// 设置拦截器生效路径
.addPathPatterns("/v1/auth/demo");
}
为了验证拦截器方法的运行顺序
我们先将AuthController类中的demo方法作为拦截的控制器方法
也在这个方法中添加一个输出
@GetMapping("/demo")
public String demo(){
System.out.println("demo方法运行");
return "hello!!! Demo!!!";
}
重启sys模块
访问:http://localhost:8001/v1/auth/demo
观察idea控制台输出内容的顺序
preHandle运行
demo方法运行
postHandle运行
afterCompletion运行
拦截器解析JWT
上面编写了拦截器的使用示例
下面要回到我们的登录流程中
继续实现不同身份跳转不同页面的功能
我们要编写一个拦截器,在HomeController类中的控制器方法运行前解析JWT并将用户信息保存到Spring-Security中
拦截器代码中一定会包含解析JWT的功能
而这个功能需要向auth模块发送Ribbon请求实现
所以要先在SpringBoot启动类中添加Ribbon的支持
knows-sys模块,在SpringBoot启动类中添加Ribbon的支持
代码如下
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("cn.tedu.knows.sys.mapper")
public class KnowsSysApplication {
public static void main(String[] args) {
SpringApplication.run(KnowsSysApplication.class, args);
}
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
有了Ribbon的支持我们解析JWT的拦截器才万事具备
继续在interceptor包中创建一个拦截器AuthInterceptor
代码如下
// 别忘了编写@Component注解
@Component
public class AuthInterceptor implements HandlerInterceptor {
// 解析Jwt需要Ribbon添加依赖
@Resource
private RestTemplate restTemplate;
// 需要用户信息的控制器运行之前,先运行这个拦截器方法
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// 1.从请求中获得JWT
String token=request.getParameter("accessToken");
// 2.向auth模块的验证令牌的方法发起Ribbon请求
String url="http://auth-service/oauth/check_token?token={1}";
// Ribbon的返回值可以使用Map来接收,自动将json的key和value赋值到map中
Map<String,Object> map=restTemplate
.getForObject(url, Map.class,token);
// 3.解析返回的用户信息(提取用户名\权限\角色)
String username=map.get("user_name").toString();
List<String> list=(List<String>)map.get("authorities");
// 4.用户信息保存为UserDetails类型对象
// Spring-Security框架规定了UserDetails类型对象为保存用户信息的对象
// 我们必须遵守框架的要求,实例化这个对象并为相关属性赋值
// UserDetails中用户权限要求时String类型数组,而我们现在是List类型
// 将List<String>转换为String[]
String[] auth=list.toArray(new String[0]);
UserDetails details= User.builder()
.username(username)
.password("")
.authorities(auth)
.build();
// 5.将UserDetails类型对象保存到Spring-Security容器中,以便控制器获取
// 这个操作的过程是Spring-Security框架规定好的,不需要我们记忆
// 需要时照搬即可
PreAuthenticatedAuthenticationToken authenticationToken
=new PreAuthenticatedAuthenticationToken(
details,details.getPassword(),
AuthorityUtils.createAuthorityList(auth));
// 关联当前请求,上面的信息保存在request对象中,以便控制器获取
authenticationToken.setDetails(
new WebAuthenticationDetails(request));
//将用户详情对象保存在Spring-Security容器中
SecurityContextHolder.getContext()
.setAuthentication(authenticationToken);
// 最后别忘了返回true
return true;
}
}
Jwt拦截器的配置
上面章节中完成了解析Jwt为用户信息并保存到Spring-Security容器中的拦截器
这个拦截器非常最重要,它能实现今后所有需要用户信息的控制器方法获得用户信息的功能
定义之后,拦截器要配置到指定路径才能生效
现在我们需要请求/v1/home时,让拦截器解析用户信息
在security包下的WebConfig类中添加拦截器生效的配置
完成根据不同身份跳转首页
编写前端发送异步请求
我们现在就差index.html向HomeController类的控制器方法发送axios请求的步骤了
我们需要将浏览器中保存的JWT当做请求的参数发送给控制器
knows-client项目
index.html页面的js代码中添加axios的调用
代码如下
// 利用jQuery提供的页面加载完毕运行的方法,运行代码
$(function(){
// 获得localStorage中的jwt
let token=localStorage.getItem("accessToken");
// 将jwt token 输出到控制台
console.log(token);
axios({
url:"http://localhost:9000/v1/home",
method:"get",
params:{
accessToken:token
}
}).then(function(response){
// response.data就是HomeController响应的字符串
// 这个字符串是根据用户身份返回的首页路径
// 直接重定向到这个路径,就可以访问对应身份的首页
location.href=response.data;
})
})
Nacos启动
gateway\auth启动
重启sys模块和client模块
一定从login.html开始访问,分别登录一次学生和讲师
测试登录效果
观察是否能够跳转到对应的首页
注意要登录的用户一定要先修改密码将{bcrypt}删除
单点登录流程小结
1.login.html输入用户名和密码,进行登录操作
2.login.html向auth服务器发送登录请求的axios目标是获取令牌
3.auth服务器会根据我们配置的信息,验证用户输入的用户名和密码是否正确,如果错误返回登录失败信息,如果正确登录成功生成jwt令牌
4.login.html在登录成功后会接收Jwt令牌,并保存在LocalStorage中
5.login.html会重定向到index.html
6.index.html会在页面加载完毕后,将localStorage中的Jwt获取出来
7.index.html会发起axios请求到/v1/home而且会将Jwt保存在请求参数中
8.拦截器会拦截目标为/v1/home的请求,获得保存在请求参数中的JWT
9.拦截器会解析获得的Jwt为用户信息,然后将它保存在Spring-Security容器中
10.在拦截器运行完毕之后/v1/home的控制器方法才会运行,判断Spring-Security容器中的角色
11.根据角色判断结果返回对应角色的首页路径
12.index.html的axios接收到这个返回的路径,实施重定向
迁移问答模块
登录的功能完成了,下面要将portal项目中的其它功能依次迁移到微服务项目中
我们首先迁移首页和问题发布页相关功能
迁移业务逻辑层
knows-faq模块
之前迁移了tag的相关内容,完成了mapper的迁移
下面开始针对Question的Service开始迁移
迁移过来,导包即可
然后迁移业务逻辑层实现类
这个类的代码导包之后还有一些错误
需要Ribbon调用才能解决
在当前类中定义下面的方法
@Resource
private RestTemplate restTemplate;
// 利用Ribbon根据用户名获得用户对象的方法
private User getUser(String username){
String url=
"http://sys-service/v1/auth/user?username={1}";
User user=restTemplate.getForObject(
url,User.class,username);
return user;
}
有3个位置需要调用这个方法
需要调用的位置编写如下代码
User user=getUser(username);
还有需要获得所有讲师Map而引发的错误
// 6.新增user_question关系表
Map<String,User> teacherMap=new HashMap<>();
// 通过Ribbon获得所有讲师的数组
String url="http://sys-service/v1/users/master";
User[] teachers=restTemplate.getForObject(
url,User[].class);
for(User u:teachers){
teacherMap.put(u.getNickname(),u);
}
//.....
实际代码到项目中对照即可
迁移控制层
导包
修改控制器路径为v2
// 别忘了修改faq模块的路由特征路径为v2开头!!!
@RequestMapping("/v2/questions")
public class QuestionController {
.....
}
配置拦截器
在faq模块中创建interceptor包
将sys模块中的AuthInterceptor拦截器复制到这个包中
下面要根据实际需求,配置拦截器生效的路径
faq模块的WebConfig类添加拦截器配置方法如下
@Resource
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns(
"/v2/questions", //发布问题
"/v2/questions/my", //学生首页
"/v2/questions/teacher" //讲师首页
);
}
修改前端请求
转到knows-client项目
我们今后在页面中会有很多请求需要JWT
所以我们建议大家在utils.js文件中编写从localStorage中获得JWT的代码
之后任何js文件都可以直接使用utils.js取出的JWT直接使用
// utils.js文件中获得localStorage中的JWT
// 其它所有js文件都可以使用
let token=localStorage.getItem("accessToken");
针对我们上面迁移的业务,找到对应的axios,修改请求路径并发送jwt
index.js
axios({
url: 'http://localhost:9000/v2/questions/my',
method: "GET",
params:{
pageNum:pageNum,
accessToken:token
}
})
index_teacher.js
axios({
url: 'http://localhost:9000/v2/questions/teacher',
method: "GET",
params:{
pageNum:pageNum,
accessToken:token
}
})
createQuestion.js
提交问题的方法:
let content = $('#summernote').val();
console.log(content);
//data 对象,与服务器端QuestionVo的属性对应
let form =new FormData();
form.append("title",this.title);
form.append("tagNames",this.selectedTags);
form.append("teacherNicknames",this.selectedTeachers);
form.append("content",content);
// form 额外添加一个accessToken属性,它不会影响对VO类正常的赋值
// 凡是post请求发送jwt,都是将jwt保存在表单中
form.append("accessToken",token);
console.log(form);
axios({
url:'http://localhost:9000/v2/questions',
method:'POST',
data:form,
})
加载所有标签的方法
axios({
url:'http://localhost:9000/v2/tags',
method: 'GET'
})
加载所有讲师的方法
axios({
url:'http://localhost:9000/v1/users/master',
method: 'GET'
})
启动faq模块
重启client项目
进行登录操作,尝试新增问题,观察是否成功
迁移用户信息面板
学生和讲师首页信息中都还没有显示用户信息面板
因为我们没有完成这个功能
下面我们要完成这个功能
编写faq模块的相关Rest接口
用户信息面板功能主要属于sys模块
但是面板中显示的问题数和收藏数又属于faq模块的业务范围
所以我们需要在sys的业务逻辑层中使用Ribbon调用faq模块提供的根据用户id查询问题数\收藏数的Rest接口
因为现在faq模块还没有准备这样的Rest接口所以要先编写它们
从业务逻辑层开始编写
转到knows-faq模块
IQuestionService
// 根据用户id查询问题数
Integer countQuestionsByUserId(Integer userId);
QuestionServiceImpl实现
@Override
public Integer countQuestionsByUserId(Integer userId) {
return questionMapper.countQuestionsByUserId(userId);
}
控制器QuestionController类中添加Rest接口
// 根据用户id查询问题数的Rest接口
@GetMapping("/count")
public Integer count(Integer userId){
return questionService.countQuestionsByUserId(userId);
}
英文
interceptor: 拦截器