Token

Token

一、介绍

  • 本质上是一个加密的字符串

  • 作用是用来代替Session

  • 生成token加密字符串,用到的技术,JWT

1、JWT

JSON Web Tokens

  • 认证生成Token字符串

二、生成Token字符串

1、引入依赖

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>

2、新建工具类

这个工具类,不需要手写,要求会修改就可以了

​
public class JWTUtil {
    //生成的服务器秘钥
    private static final String keyStr = "Oon2+pDRDSxQ8uVt4zuDc1ogr9p0JjVJlNd2aVMqPWE=";
​
    private static final Integer expTime = 1000 * 60 * 60 * 24 * 15;
​
    //加密的方法,把uid加密成Token字符串
    public static String generateUid(String uid){
        Map<String,Object> map = new HashMap<>();
        map.put("uid",uid);
        //因为加密的字符串,要给一个过期时间
        //记录什么时候生成的Token
        Date time = new Date();
        //设置过期时间,这个字符串多久过期失效
        Date lastTime = new Date(time.getTime() + expTime);
        //生成一个Key
        SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(keyStr));
        return Jwts.builder()
                .setClaims(map) //加密的数据
                .setIssuedAt(time)  //标准签名,加密的时间
                .setExpiration(lastTime)    //过期时间
                .signWith(key)  //指定签名Key,Header中的秘钥
                .compact();
    }
    //解密
    public static String parse(String token){
        //解密 获取和加密一样的key
        SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(keyStr));
​
        String string = null;
        try {
            Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            Claims body = jws.getBody();
            //获取加密前的字符串
            string = body.get("uid", String.class);
        } catch (ExpiredJwtException e) {
            throw new JavasmException(ExceptionEnum.Token_Expired_Error);
        } catch (UnsupportedJwtException e) {
            throw new JavasmException(ExceptionEnum.Token_Unsupported_Error);
        } catch (SignatureException e) {
            throw new JavasmException(ExceptionEnum.Token_Signature_Error);
        } catch (Exception e) {
            throw new JavasmException(ExceptionEnum.SYSTEM_ERROR);
        }
        return string;
    }
​
    //生成服务端知道的秘钥,只有服务端能知道,不能泄露
    //不能随意更改,如果秘钥更改了,之前生成的所有Token都会失效
    //这个方法不会频繁执行,只会在生产秘钥的时候执行1次
    private static void  generateKey(){
        //指定算法
        SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        //生成加密的字符串
        String encode = Encoders.BASE64.encode(key.getEncoded());
​
        System.out.println(encode);
    }
​
    public static void main(String[] args) {
        //generateKey();
/*        String string = generateUid("100");
        System.out.println(string);*/
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiIxMDAiLCJpYXQiOjE3NjQ3Mjc0ODQsImV4cCI6MTc2NDcyNzQ5OX0.drNbsw8H61R2EUNJpgC0Yfe6E8M1TogHSpMwXxM1Gbg";
        String uid = parse(token);
        System.out.println(uid);
    }
​
}
​

三、登录案例

新的登录流程

  1. 登录成功之后,返回Token字符串

  2. Token字符串,随着Header对象,返回客户端

  3. 客户端要保存Token字符串,到本地永久存储

  4. 每次请求服务器,都携带Token

  5. 服务器端,在拦截器中判断,Token是否失效,决定是否允许继续访问接口

  6. 增加1个自动登录功能

    • 如果Token不失效的情况下,访问首页的时候,自动帮助我们进行登录操作

1、修改现有登录代码

  • 因为是修改登录逻辑,要保持原有的代码不变

  • 保持原有登录接口的责任单一,所以使用AOP+注解

  • 使用AOP扩展现有的功能

    • 获取返回的数据,WebUser,

    • 根据uid生成Token字符串

    • 字符串存入Header

  • 新建一个自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SaveToken {
}
  • 新建切面

@Component
@Aspect
public class SaveTokenAspect {
​
    @Resource
    HttpServletResponse response;
​
    @AfterReturning(
            value = "@annotation(com.javasm.qingqing.common.annotation.SaveToken)",
            returning = "webUser"
    )
    public void f1(JoinPoint joinPoint,Object webUser){
        if (webUser instanceof WebUser){
            WebUser loginUser = (WebUser) webUser;
            //获取uid
            Integer uid = loginUser.getUid();
            //生成Token字符串
            String token = JWTUtil.generateUid(uid.toString());
            //把token加入Header对象
            response.addHeader(JWTUtil.Server_Token,token);
            //默认情况下,前端js是不会随便处理别人给的Header数据的
            //需要额外配置一下,让js来处理自定义header
            response.setHeader("Access-Control-Expose-Headers",JWTUtil.Server_Token);
        }
    }
}
​
  • 使用

    @Override
    @WebLoginLog
    @SaveToken
    public WebUser doUnameLogin(String username, String password) {。。。}

2、修改拦截器

自定义注解+拦截器

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auth {
}

拦截器

@Component
public class LoginInterceptor implements HandlerInterceptor {
​
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        //默认,所有的接口,都能不登录就可以访问,只有加了@Auth的接口,才需要登录才能访问
        if (handler instanceof HandlerMethod){
            //说明 访问的是Controller的方法,而不是其他的地址
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            //当前正在被访问的方法对象
            Method method = handlerMethod.getMethod();
            //判断,方法上面,是否有@Auth注解
            Auth auth = method.getAnnotation(Auth.class);
            if (auth == null){
                return true;
            }
            //代码执行到这里说明已经加了@Auth,所以必须登录才能访问
            //从Header中获取Token
            String token = request.getHeader(JWTUtil.Server_Token);
            if (StringUtils.isEmpty(token)){
                //如果没有传token,不让登录
                throw new JavasmException(ExceptionEnum.Token_Expired_Error);
            }
            //如果代码运行到这里,说明token不是null
            JWTUtil.parse(token);
            return true;
        }
        return true;
    }
}
  • 修改配置文件,配置拦截器

@Component
public class JavasmWebConfiguration implements WebMvcConfigurer {
​
    @Autowired
    LoginInterceptor loginInterceptor;
​
    //注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login/**");
    }
}

3、修改页面

查看request.js

// 请求拦截器
service.interceptors.request.use(
    config => {
        const token = tokenStore().token;
        if (token) {
            config.headers.javasm_token = token;
        }
        if (config.method === 'post') {
            // 检查是否已经设置 Content-Type 为 application/json
            if (!config.headers['Content-Type'] || !config.headers['Content-Type'].includes('multipart/form-data')) {
                // 只有当 Content-Type 不是 application/json 时才进行 qs.stringify
                if (!config.headers['Content-Type'] || !config.headers['Content-Type'].includes('application/json')) {
                    config.data = qs.stringify(config.data);
                }
            }
        }
        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

// 响应拦截器
service.interceptors.response.use(
    response => {
        const newToken = response.headers.javasm_token;
        if (newToken) {
            tokenStore().token = newToken;
        }
        if (response.status === 200) {
            return Promise.resolve(response.data);
        } else {
            return Promise.reject(response);
        }
    },
    error => {
        alert(`异常请求:${JSON.stringify(error.message)}`);
        return Promise.reject(error);
    }
);

4、自动登录

确保所有的页面,打开的时候,都执行自动登录方法

  • 服务端自动登录接口

    @Resource
    HttpServletRequest request;

    @PostMapping("/auto")
    public R doUidLogin(){
        //获取token信息
        String token = request.getHeader(JWTUtil.Server_Token);
        if (token == null){
            throw new JavasmException(ExceptionEnum.Token_Expired_Error);
        }
        String uid = JWTUtil.parse(token);
        //代码执行到这里,没有抛出异常,说明已经是一个正常的token字符串了
        WebUser webUser = loginService.doUidLogin(uid);
        return R.ok(webUser);
    }
  • Service

    @Override
    @WebLoginLog
    @SaveToken
    public WebUser doUidLogin(String uid) {
        String key = String.format(RedisKeys.WebUserId,uid);
        WebUser webUser = redisTemplate.opsForValue().get(key);
        if (webUser != null){
            return webUser;
        }
        //假设缓存过期没有查询到
        WebUser loginUser = webUserService.getById(uid);
        if (loginUser != null){
            //用户详情
            WebUserInfo userInfo = webUserInfoService.getById(uid);
            loginUser.setUserInfo(userInfo);
            //用户财产
            WebUserPossession possession = webUserPossessionService.getById(uid);
            loginUser.setPossession(possession);
            //存到Redis
            redisTemplate.opsForValue().set(key,loginUser,15,TimeUnit.DAYS);
        }else {
            redisTemplate.opsForValue().set(key,new WebUser(),3,TimeUnit.MINUTES);
        }

        return loginUser;
    }
  • 页面

应该在页面加载的时候,判断,当前是否已经登录了

如果当前是已经登录的状态,就不需要再去自动登录了

如果现在是未登录状态,再去执行自动登录方法

自动登录返回的数据,正常存储到userSotre中

  • 修改Top.vue

<template>
  <div class="container-fluid">
    <div class="row top " >
      <nav class="navbar navbar-expand-lg bg-primary col" data-bs-theme="dark">
        <div class="container-fluid">
          <router-link class="navbar-brand top-logo" to="/login">
            <img src="@/assets/imgs/logo_top.png" class="rounded float-start" alt="...">
          </router-link>
          <div class="collapse navbar-collapse top-info" id="navbarSupportedContent">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
              <li class="nav-item">
                <router-link class="nav-link" aria-current="page" to="/">首页</router-link>
              </li>
              <li class="nav-item">
                <router-link class="nav-link" aria-current="page" to="/room/free">组队开黑</router-link>
              </li>
              <li class="nav-item">
                <router-link class="nav-link" aria-current="page" to="/godPlayers">大神陪玩</router-link>
              </li>
              <li class="nav-item">
                <router-link class="nav-link" aria-current="page" to="/login">派单大厅</router-link>
              </li>
              <li class="nav-item">
                <router-link class="nav-link" aria-current="page" to="/login">娱乐大厅</router-link>
              </li>
            </ul>
            <form class="d-flex option-item" role="search">
              <input class="form-control me-2 bg-white" type="search" placeholder="房间号/用户昵称" aria-label="Search">
              <button class="btn btn-outline-light" type="button">
                <i class="bi bi-search-heart"></i>
              </button>
            </form>
            <template v-if="isLogin">
              <div class="option-item">
                <img :src="userModel.userInfo.headPic" class="head_pic" alt="..." data-bs-toggle="modal" data-bs-target="#headModal">
                <span class="nickname">{{userModel.userInfo.nickname}}</span>
              </div>
              <div class="option-item">
                <div class="btn-group" data-bs-theme="light">
                  <button type="button" class="btn btn-primary dropdown-toggle "
                          data-bs-toggle="dropdown" aria-expanded="false"
                          data-bs-offset="10,20">
                    <i class="bi bi-gear bi-white"></i>
                  </button>
                  <ul class="dropdown-menu dropdown-menu-md-end">
                    <li>
                      <router-link class="dropdown-item" to="/login">个人信息</router-link>
                    </li>
                    <li>
                      <router-link class="dropdown-item" to="/login">修改密码</router-link>
                    </li>
                    <li>
<!--                      <router-link class="dropdown-item" to="/login">退出登录</router-link>-->
                      <a class="dropdown-item" @click="logout">退出登录</a>
                    </li>
                  </ul>
                </div>
              </div>
            </template>
            <template v-else>
              <router-link class="btn btn-outline-light" to="/login">登录</router-link>
              <router-link class="btn btn-outline-light" to="/reg">注册</router-link>
            </template>

          </div>
        </div>
      </nav>
    </div>

  </div>

  <div class="modal fade" id="headModal" tabindex="-1" aria-labelledby="headModalLabel" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-body">
          <img :src="userModel.userInfo.headPic" class="head_pic" alt="..." data-bs-dismiss="modal">
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import {ref, reactive, onMounted} from "vue";
import router from '@/router/index.js'
import userStore from '@/stores/userStore.js';
import api from '@/utils/request.js';
import {ElMessage} from "element-plus";
import TokenStore from "@/stores/TokenStore.js";

let user = userStore();
let isLogin = ref(false);
let userModel = reactive(user.userModel);

let autoLogin=()=>{
  api.post("/login/auto").then(result=>{
    if (result.code === 200){
      userModel = result.data;
      userStore().userModel = result.data;
      isLogin.value = true;
    }
  })
}

let logout = ()=>{
  //清空本地Token
  TokenStore().token = "";
  //清空本地用户信息
  userStore().userModel = {
    uid: -1,
    userInfo:{
      headPic: "http://cd.ray-live.cn/imgs/headpic/pic_54.jpg"
    }
  }
  router.push("/login")
}

onMounted(()=>{
  console.log(userModel)
  //判断是否登录
  if (user.userModel.uid!==-1){
    isLogin.value = true;
  }else {
    //未登录
    autoLogin()
  }
})
</script>

<style scoped>

</style>

远程接口调用

  • 在Java的服务器,访问其他的接口的技术

  • 支持Get、Post、Put、Delete请求

  • 不需要引入额外的依赖

1、搭建环境

在一个配置类中,把RestTempalte注入到Spring容器

@Configuration
@EnableTransactionManagement
public class ServerConfig {

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

2、Get测试

2.1 调用自己的服务

  • 启动调用一方的服务(qingqing 8080)

  • 启动被调用一方的服务(mpdemo 8088)

  • 每次调用之前,都要确保被调用一方接口,可以正常访问

  • 图中,左边调用右边

2.2 被调用一方的代码
    @GetMapping("/list")
    public R list(){
        return R.ok(skillService.list());
    }

测试通过

2.3 调用一方的代码
@RestController
@RequestMapping("/skill")
public class SkillController {

    @Resource
    RestTemplate restTemplate;

    @GetMapping("/all/list")
    public R queryList(){
        String url = "http://127.0.0.1:8088/skill/list";
        //相当于浏览器获取到了Response对象
        ResponseEntity<R> rResponseEntity = restTemplate.getForEntity(url, R.class);
        //获取body信息
        R body = rResponseEntity.getBody();
        //获取头信息
        HttpHeaders headers = rResponseEntity.getHeaders();

        return body;
    }

}

3、Get同步新闻列表

https://www.tianapi.com/

3.1 同步电竞新闻列表
@RestController
@RequestMapping("/news")
public class NewsController {

    @Resource
    NewsService newsService;

    //同步新闻列表
    @GetMapping("/sync")
    public R syncNewsList(){
        newsService.syncNewsList();
        return R.ok();
    }

    @GetMapping("/list")
    public R queryNewsList(){
        List<NewsVo> newsList = newsService.list();
        return R.ok(newsList);
    }
}
  • Service

@Service
public class NewsServiceImpl implements NewsService {

    @Resource
    RestTemplate restTemplate;

    private static final String key = "b8726ecc2f74b3d9f57d808c95b01b19";
    private static final String esportsUrl = "https://apis.tianapi.com/esports/index";

    @Resource
    RedisTemplate<String,NewsVo> redisTemplate;

    @Override
    public List<NewsVo> syncNewsList() {
        //同步新闻列表
        String url = esportsUrl+"?key="+key+"&num=50";
        //restTemplate 自动把json字符串,转成了对象,来使用
        NewsResult newsResult = restTemplate.getForObject(url, NewsResult.class);
        if (newsResult !=null && newsResult.getCode().equals(200)){
            //把数据存入Redis
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy:MM:dd");
            String today = simpleDateFormat.format(new Date());
            String redisKey = String.format(RedisKeys.NewsDayList,today);
            List<NewsVo> newslist = newsResult.getResult().getNewslist();
            redisTemplate.opsForList().rightPushAll(redisKey,newslist);
            //给个过期时间
            redisTemplate.expire(redisKey,1, TimeUnit.HOURS);
            return newslist;
        }
        return null;

    }

    @Override
    public List<NewsVo> list() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy:MM:dd");
        String today = simpleDateFormat.format(new Date());
        String redisKey = String.format(RedisKeys.NewsDayList,today);
        List<NewsVo> list = redisTemplate.opsForList().range(redisKey, 0, -1);
        if (list == null || list.isEmpty()){
            list = syncNewsList();
        }
        return list;
    }
}

4、Post

4.1 被调用一方的代码
@PostMapping("/add1")
public R insert(@RequestBody Skill skill) {
    return R.ok(this.skillService.save(skill));
}
@PostMapping("/add2")
public R insert2(Skill skill){
    return R.ok(this.skillService.save(skill));
}
4.2 调用一方的代码
    @GetMapping("/save/{name}/{price}")
    public R save(@PathVariable String name,@PathVariable Double price){
        //普通参数,表单?形式
        String url = "http://127.0.0.1:8088/skill/add2";
        //拼装参数
        MultiValueMap<String,Object> map = new LinkedMultiValueMap<>();
        map.add("gameUsername",name);
        map.add("skillPrice",price);
        //设置 header对象
        HttpHeaders headers = new HttpHeaders();
        //application/x-www-form-urlencoded  表单提交
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        //设置Request对象
        HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(map, headers);
        //发起请求
        ResponseEntity<R> rResponseEntity = restTemplate.postForEntity(url, request, R.class);
        return rResponseEntity.getBody();
    }
  • Json参数

@Data
public class SkillSaveVo {
    //这里的属性名,和被调用一方的参数json字符串匹配
    private String gameUsername;
    private Double skillPrice;
}
    @GetMapping("/save1/{name}/{price}")
    public R save1(@PathVariable String name,@PathVariable Double price){
        //普通参数,表单?形式
        String url = "http://127.0.0.1:8088/skill/add1";
        //拼装参数
        SkillSaveVo vo = new SkillSaveVo();
        vo.setGameUsername(name);
        vo.setSkillPrice(price);
        //设置header的信息
        HttpHeaders headers = new HttpHeaders();
        //application/json
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<SkillSaveVo> request = new HttpEntity<>(vo, headers);
        //发起请求
        R r = restTemplate.postForObject(url, request, R.class);
        return r;
    }

5、Put请求

5.1 被调用一方
@PutMapping("/update2")
public R update2(Skill skill){
    return R.ok(this.skillService.updateById(skill));
}
5.2 调用一方
    @GetMapping("/update")
    public R update(Integer id,String name){
        String url = "http://127.0.0.1:8088/skill/update2";
        MultiValueMap<String,Object> map = new LinkedMultiValueMap<>();
        map.add("skillId",id);
        map.add("gameUsername",name);

        restTemplate.put(url,map);
        return R.ok();
    }

6、Delete请求

6.1 被调用一方
    @DeleteMapping("/del/{id}")
    public R deleteById(@PathVariable Integer id){
        skillService.removeById(id);
        return R.ok();
    }
6.2 调用一方
@GetMapping("/del/{id}")
public R delById(@PathVariable Integer id){
    String url = "http://127.0.0.1:8088/skill/del/{0}";
    restTemplate.delete(url,id);
    return R.ok();
}

7、通用

7.1 被调用一方
​
    @PutMapping("/update1")
    public R update(@RequestBody Skill skill) {
        return R.ok(this.skillService.updateById(skill));
    }
7.2 调用一方
@GetMapping("/f1")
public R f1(Integer id,String name){
    String url = "http://127.0.0.1:8088/skill/update1";
    //组装参数
    SkillSaveVo vo = new SkillSaveVo();
    vo.setSkillId(id);
    vo.setGameUsername(name);
    //header
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    //生成Token
    String token = JWTUtil.generateUid(id.toString());
    headers.add(JWTUtil.Server_Token,token);
    //body
    HttpEntity<SkillSaveVo> body = new HttpEntity<>(vo, headers);
    //发起调用
    ResponseEntity<R> exchange = restTemplate.exchange(url, HttpMethod.PUT, body, R.class);
​
    return exchange.getBody();
}

总结

重点

  • 修改 Token工具类

  • 登录案例

了解

  • 远程接口调用API尽量掌握

  • 尽量把每个类型的案例都写一遍

  • 不要求记住API

  • 会使用远程接口调用技术,去访问其他人的网站和接口

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值