1.集成jwt验证
session模式:
某些url是公开的,如login、registe,他们不需要让用户登录。登陆成功后生成session到服务器端。(多个服务器的话就要存多个)
其余的url是不公开的,需要用户先登录才能访问。要判断session是否有效,然后将session当中的user信息提取到上下文中,再访问controller。
jwt验证方式:方便前后端分离的跨域问题而且jwt-token是存在于本地浏览器中
加入依赖:jjwt-api、jjwt-impl、jjwt-jackson
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
实现utils.JwtUtil类,为jwt工具类,用来创建、解析jwt token
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // 有效期14天
public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("sg")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
}
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}
}
实现config.filter.JwtAuthenticationTokenFilter类,用来验证jwt token,如果验证成功,则将User信息注入上下文中
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
token = token.substring(7);
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException(e);
}
User user = userMapper.selectById(Integer.parseInt(userid));
if (user == null) {
throw new RuntimeException("用户名未登录");
}
UserDetailsImpl loginUser = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
配置config.SecurityConfig类,放行登录、注册等接口
import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(CsrfConfigurer::disable) // 基于token,不需要csrf
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 基于token,不需要session
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/user/account/token", "/user/account/register").permitAll() // 放行api
.requestMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
2.创建后端api
将数据库中的id域变为自增
在pojo.User类中添加注解:@TableId(type = IdType.AUTO)
实现/user/account/token/:验证用户名密码,验证成功后返回jwt token(令牌)
实现/user/account/info/:根据令牌返回用户信息
实现/user/account/register/:注册账号
2.1实现登录功能
package com.kob.backend.service.impl.user.account;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.service.user.account.LoginService;
import com.kob.backend.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public Map<String, String> getToken(String username, String password) {
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authenticate = authenticationManager.authenticate(token); //这里如果登录失败,会自动报错
UserDetailsImpl loginUser = (UserDetailsImpl) authenticate.getPrincipal();
User user = loginUser.getUser();
String jwt = JwtUtil.createJWT(user.getId().toString());
Map<String, String> map = new HashMap<String, String>();;
map.put("error_message","success"); //如果执行到这里一定是成功的
map.put("token",jwt);
return map;
}
}
//可以这样根据失败结果返回给用户
try {
Authentication authenticate = authenticationManager.authenticate(token);
// 登录成功的处理逻辑
} catch (AuthenticationException e) {
// 登录失败的处理逻辑
// 根据异常类型进行适当的处理
if (e instanceof BadCredentialsException) {
// 处理用户名或密码错误
} else if (e instanceof DisabledException) {
// 处理用户被禁用
} else if (e instanceof LockedException) {
// 处理用户被锁定
} else if (e instanceof AccountExpiredException) {
// 处理用户帐户已过期
} else if (e instanceof CredentialsExpiredException) {
// 处理用户凭据(密码)已过期
} else {
// 其他类型的身份验证异常
}
}
@RestController
public class LoginController {
@Autowired
LoginService loginService;
@PostMapping("/user/account/token/")
public Map<String,String> getToken(@RequestParam Map<String,String> map) {
String username = map.get("username");
String password = map.get("password");
return loginService.getToken(username,password);
}
}
2.2.实现注册功能
@Service
public class RegisterServiceImpl implements RegisterService {
@Autowired
private UserMapper userMapper;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public Map<String, String> register(String username, String password, String confirmedPassword) {
Map<String,String> map = new HashMap<String,String>();
if(username == null) {
map.put("error_message","用户名不能为空");
return map;
}
if(password == null || confirmedPassword == null) {
map.put("error_message","密码或者确认密码不能为空");
return map;
}
if(!password.equals(confirmedPassword)) {
map.put("error_message","两次密码不一致");
return map;
}
username = username.trim();
if(username.length() == 0) {
map.put("error_message","用户名不能为空");
return map;
}
if(username.length() > 100) {
map.put("error_message","用户名长度不能大于100");
return map;
}
if(password.length() > 100 || confirmedPassword.length() > 100) {
map.put("error_message","密码长度不能大于100");
return map;
}
if (password.length() == 0 || confirmedPassword.length() == 0) {
map.put("error_message","密码不能为空");
return map;
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
if(userMapper.selectCount(queryWrapper) > 0) {
map.put("error_message","用户名重复");
return map;
}
String encodedPassword = passwordEncoder.encode(password);
User user = new User(null,username,encodedPassword,"https://cdn.acwing.com/media/user/profile/photo/143543_lg_7ef77402b7.jpg");
userMapper.insert(user);
map.put("error_message","注册用户成功");
return map;
}
}
@RestController
public class RegisterController {
@Autowired
private RegisterService registerService;
@PostMapping("/user/account/register/")
public Map<String,String> register(@RequestParam Map<String,String> map) {
String username = map.get("username");
String password = map.get("password");
String confirmedPassword = map.get("confirmedPassword");
System.out.println("Asd");
return registerService.register(username,password,confirmedPassword);
}
}
2.3获取用户信息的功能
@Service
public class InfoServiceImpl implements InfoService {
@Override
public Map<String, String> getInfo() {
//上下文当中提取用户信息
UsernamePasswordAuthenticationToken authenticationToken =
(UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
UserDetailsImpl loginUser = (UserDetailsImpl)authenticationToken.getPrincipal();
User user = loginUser.getUser();
Map<String, String> info = new HashMap<String, String>();
info.put("error_message","success");
info.put("id",user.getId().toString());
info.put("username",user.getUsername());
info.put("photo",user.getPhoto());
return info;
}
}
@RestController
public class InfoController {
@Autowired
private InfoService infoService;
@GetMapping("/user/account/info/")
public Map<String,String> getInfo() {
return infoService.getInfo();
}
}
2.4前端向后端发送的请求
setup() {
$.ajax({
url: "http://127.0.0.1:3000/user/account/token/",
type: "post",
data: {
username: 'cjb',
password: 'pcjb',
},
success(resp) {
console.log(resp);
},
error(resp) {
console.log(resp);
}
});
$.ajax({
url: "http://127.0.0.1:3000/user/account/info/",
type: "get",
headers: {
Authorization:"Bearer " + "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjZjE4ZTU5OWE4MTM0MGEzOWMwN2E4ZjY2OGQ4YmE2MSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTcxMzM1MzMxNCwiZXhwIjoxNzE0NTYyOTE0fQ.3h6I4bbuX44Jy5ReCi_i6VPbtPZFhXXcGDhmHPy5eCw"
},
success(resp) {
console.log(resp);
},
error(resp) {
console.log(resp);
}
});
$.ajax({
url: "http://127.0.0.1:3000/user/account/register/",
type: "post",
data: {
username: "e",
password: "pe",
confirmedPassword: "pe",
},
success(resp) {
console.log(resp);
},
error(resp) {
console.log(resp);
}
})
3.前端界面
使用vuex当中的store维护全局状态,如用户id、用户名、photo、token信息、是否登录is_login;
当然这些信息写在store文件下的user.js文件夹里面,然后把user.js抛出,index.js引入user.js。
其中user.js里面的mutations里面写直接修改全局变量的方法,供给actions里面的函数进行使用,这里使用context.commit()进行调用的。
actions里面的函数是供给外面的js使用的,使用store.dispatch()进行调用。
import { createStore } from 'vuex'
import UserModule from './user'
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
user: UserModule
}
})
import $ from 'jquery'
export default {
state: { //全局信息
id:"",
username:"",
photo:"",
token:"",
is_login:false
},
getters: {
},
mutations: { //修改信息
updateUser(state,user) {
state.id = user.id,
state.username = user.username,
state.photo = user.photo,
state.is_login = user.is_login
},
updateToken(state,token) {
state.token = token
},
logout(state) {
state.id = "",
state.username = "",
state.photo = "",
state.token = "",
state.is_login = false
}
},
actions: {
login(context,data) {
$.ajax({
url: "http://127.0.0.1:3000/user/account/token/",
type: "post",
data: {
username: data.username,
password: data.password,
},
success(resp) {
if(resp.error_message === 'success') {
context.commit("updateToken",resp.token);
data.success(resp);
} else {
data.error(resp);
}
},
error(resp) {
data.error(resp);
}
});
},
getinfo(context,data) {
$.ajax({
url: "http://127.0.0.1:3000/user/account/info/",
type: "get",
headers: {
Authorization:"Bearer " + context.state.token,
},
success(resp) {
if(resp.error_message === 'success') {
context.commit("updateUser",{
...resp, //解析resp的内容
is_login: true,
});
data.success(resp);
} else {
data.error(resp);
}
},
error(resp) {
data.error(resp);
}
});
},
logout(context) {
context.commit("logout");
},
},
modules: {
}
}
navbar的修改:已登录显示用户名,未登录显示登录注册界面。这里就要用store里面的is_login来进行判断了。
<ul class="navbar-nav" v-if="$store.state.user.is_login">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ $store.state.user.username }}
</a>
<ul class="dropdown-menu">
<li>
<router-link class="dropdown-item" :to="{name:'user_bot_index'}">我的Bots</router-link>
</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" @click="logout">退出</a></li>
</ul>
</li>
</ul>
<ul class="navbar-nav" v-else>
<li class="nav-item dropdown">
<router-link class="nav-link " :to="{name : 'user_account_register'}" role="button">
注册
</router-link>
</li>
<li>
<router-link class="nav-link " :to="{name : 'user_account_login'}" role="button">
登录
</router-link>
</li>
</ul>
退出函数:清空user.js里面变量的值。
4.改进
4.1强制让用户登录,不登陆访问不了其余界面,即前端页面授权
使用router机制来实现,在每个路由上加上属性meta { requestAuth:true/false},表示是否需要收授权。使用router的beforeEach函数来实现授权,在里面判断是否需要授权并且是否登录?
import { createRouter, createWebHistory } from 'vue-router'
import PkIndexView from '../views/pk/PkIndexView'
import RankListIndexView from '../views/ranklist/RankListView'
import RecordIndexView from '../views/record/RecordIndexView'
import UserBotIndexView from '../views/user/bot/UserBotIndexView'
import NotFound from '../views/error/NotFound'
import UserAccountLoginView from '../views/user/account/UserAccountLoginView'
import UserAccountRegisterView from '../views/user/account/UserAccountRegisterView'
import store from '../store/user'
const routes = [
//默认路由是pk界面
{
path:"/",
name:"home",
redirect:"/pk/",
meta: {
requestAuth: true //是否需要登录授权才能访问
}
},
{
path:"/pk/",
name:"pk_index",
component:PkIndexView,
meta: {
requestAuth: true
}
},
{
path:"/record/",
name:"record_index",
component:RecordIndexView,
meta: {
requestAuth: true
}
},
{
path:"/ranklist/",
name:"ranklist_index",
component:RankListIndexView,
meta: {
requestAuth: true
}
},
{
path:"/user/bot/",
name:"user_bot_index",
component:UserBotIndexView,
meta: {
requestAuth: true
}
},
{
path:"/user/account/login/",
name:"user_account_login",
component: UserAccountLoginView,
meta: {
requestAuth: false
}
},
{
path:"/user/account/register/",
name:"user_account_register",
component: UserAccountRegisterView,
meta: {
requestAuth: false
}
},
{
path:"/404/",
name:"404",
component:NotFound,
meta: {
requestAuth: false
}
},
{
//匹配其他所有的非法的url
path:"/:catchAll(.*)",
redirect:"/404/"
}
]
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to,from,next) => {
if(to.meta.requestAuth && !store.state.is_login) {
next({name: "user_account_login"});
} else {
next();
}
})
export default router
4.2实现注册界面
4.3登录状态的持久化,即将token存放在本地
原本token是存在与stroe当中的,即存在于内存当中,这样一刷新token信息就会消失。
登陆时将用户信息存到localStorage当中,退出时将localStorage清空。
因为没有登录的话会被强制跳转到登录界面,所以只需要在登陆见面获取localStorage的信息,如果获取到信息,说明有用户登录,更新vuex当中store中的user相关信息,并且跳转到主页面.
小优化:每次刷新都会有一瞬间的白色见面。这是因为先到的登录界面,然后又到了主界面。瞬间的刷新导致的。所以我们可以事先进行判断是否要到登录界面(有token不用到,其余情况到)