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);
}
}
三、登录案例
新的登录流程
登录成功之后,返回Token字符串
Token字符串,随着Header对象,返回客户端
客户端要保存Token字符串,到本地永久存储
每次请求服务器,都携带Token
服务器端,在拦截器中判断,Token是否失效,决定是否允许继续访问接口
增加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同步新闻列表
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万+

被折叠的 条评论
为什么被折叠?



