ProjectDay16

实现登录首页

显示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: 拦截器

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值