1、前端调用登录接口,往接口里传入账号,密码
2、根据账号判断是否有这个用户,如果有则继续判断密码是否正确
3、验证成功后,则是根据账号,登录时间生成token(用JWT)
4、将token存入Redis当中,用于token过期策略
5、将token和用户信息返回给前端
6、此后调用后端任何接口都会先判断发来请求里token是否存在、有效(拦截器实现)
7、然后继续接下来的正常调用
1、再登录接口中实现账号、密码的验证和token创建
2、实现一个拦截器,拦截除登录接口外的其他所有接口
3、再拦截器中从请求头中取出token对其进行正确性的验证
4、然后从redis
里取出属于这个账号的token,如果取的出来则说明这个用户登录状态没有过期,然后和取出来的token进行对比,如果不一样则说明同一账号再其他地方进行了登录,则被挤出登录状态。 5、再这整个判断过程中所产生的异常(没有登录状态,不存在这个用户....)都是由全局异常处理器进行捕获然后返回给前端。
pom文件要引的依赖:
- <!--版本-->
- <properties>
- <java.version>1.8</java.version>
- <lombok.version>1.18.12</lombok.version>
- <hutool.version>5.8.8</hutool.version>
- <mybatis-plus.version>3.5.2</mybatis-plus.version>
- <JWT.version>6.0</JWT.version>
- </properties>
-
-
-
- <!--JWT-->
- <dependency>
- <groupId>com.nimbusds</groupId>
- <artifactId>nimbus-jose-jwt</artifactId>
- <version>${JWT.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.qcby</groupId>
- <artifactId>qcby-common</artifactId>
- <version>1.0-SNAPSHOT</version>
- </dependency>
-
- <!--redis-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>${lombok.version}</version>
- </dependency>
-
- <!--hutool-->
- <dependency>
- <groupId>cn.hutool</groupId>
- <artifactId>hutool-all</artifactId>
- <version>${hutool.version}</version>
- </dependency>
对于token我使用了JWT,有一个token的工具类用于token的创建和 验证。(这个类里使用的redisUtil类大家可以从网上随便找一个redis工具类,绑定上自己的reids)
- import com.nimbusds.jose.*;
- import com.nimbusds.jose.crypto.MACSigner;
- import com.nimbusds.jose.crypto.MACVerifier;
- import com.nimbusds.jwt.JWTClaimsSet;
- import com.nimbusds.jwt.SignedJWT;
- import com.qcby.framework.common.exception.ServiceException;
- import org.springframework.stereotype.Component;
-
- import javax.annotation.Resource;
- import java.text.ParseException;
- import java.util.Date;
- import java.util.Objects;
- import java.util.concurrent.TimeUnit;
-
- @Component
- public class TokenUtil {
- @Resource
- RedisUtil redisUtil;
-
- /**
- * 创建秘钥
- */
- private static final byte[] SECRET = "qngChengBoYa-realtimeWuIngWangJiaQiZhangYv".getBytes();
-
- /**
- * 生成token
- * @param account
- * @return {@link String}
- */
- public String buildToken(String account) {
-
- try {
- /**
- * 1.创建一个32-byte的密匙
- */
- MACSigner macSigner = new MACSigner(SECRET);
- /**
- * 2. 建立payload 载体
- */
- JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
- .subject("login")
- .claim("ACCOUNT",account)
- .issueTime(new Date())
- .build();
-
- /**
- * 3. 建立签名
- */
- SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
- signedJWT.sign(macSigner);
-
- /**
- * 4. 生成token
- */
- String token = signedJWT.serialize();
- redisUtil.setEx(account,token,10,TimeUnit.MINUTES);
- return token;
- } catch (KeyLengthException e) {
- e.printStackTrace();
- } catch (JOSEException e) {
- e.printStackTrace();
- }
- return null;
- }
-
- /**
- * 校验token
- * @param token
- * @return
- */
- public boolean verifyToken(String token) {
-
- try {
- SignedJWT jwt = SignedJWT.parse(token);
- JWSVerifier verifier = new MACVerifier(SECRET);
-
- /**
- * 校验是否有效
- */
- if (!jwt.verify(verifier)) {
- return false;
- }
- /**
- * 获取载体中的数据
- */
- String account = (String) jwt.getJWTClaimsSet().getClaim("ACCOUNT");
- //是否有
- if (Objects.isNull(account)){
-
- return false;
- }
- /**
- * 判断redis里是否有account为key的值,如果有
- * 判断token是否和redis里存的是是否一样,
- * 如果不一样说明已经有其他账号登录了,则回到登录页面
- * 如果一样,则给token续期
- */
- if (redisUtil.hasKey(account)){
- String s = redisUtil.get(account);
- if (s.equals(token)){
- redisUtil.expire(account,10,TimeUnit.MINUTES);
- return true;
- }
- throw new ServiceException("422","有其他设备登录");
- }
- } catch (ParseException e) {
- e.printStackTrace();
- } catch (JOSEException e) {
- e.printStackTrace();
- }
- return false;
- }
-
-
-
- }
登录拦截器的实现,要先创建一个类并实现HandlerInterceptor这个接口,然后再创建一个拦截器的配置类令其实现WebmvcConfigurer这个接口,重新addInterceptors方法,再这个方法中将之前实现的登录拦截器给注册进去,并配置这个拦截器的拦截路径,拦截优先级等等。
- /**
- * 请求拦截器
- * @author MI
- * @date 2023/10/03
- */
- @Component
- public class LoginInterceptor implements HandlerInterceptor {
- private static Logger log = Logger.getLogger(LoginInterceptor.class);
- /***
- * 在请求处理之前进行调用(Controller方法调用之前)
- @param request
- @param response
- @param handler
- @return boolean
- @throws Exception
- */
- @Resource
- TokenUtil tokenUtil;
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
-
- /**
- * 从请求头中取出token,并判断其是否存在和合法
- */
- String token = request.getHeader("token");
- if (token != null && tokenUtil.verifyToken(token)) {
- return true;
- }else {
- throw new ServiceException("100","还未登录");
- }
- }
-
- /***
- * 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
- @param request
- @param response
- @param handler
- @param modelAndView
- @throws Exception
- */
- @Override
- public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
- }
-
- /***
- * 整个请求结束之后被调用,也就是在DispatchServlet渲染了对应的视图之后执行(主要用于进行资源清理工作)
- @param request
- @param response
- @param handler
- @param ex
- @throws Exception
- */
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- }
- }
-
-
-
- import javax.annotation.Resource;
- @Configuration
- public class MyWebMvcConfig implements WebMvcConfigurer {
- @Resource
- LoginInterceptor loginInterceptor;
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- /**
- * 登录拦截器
- * */
- registry.addInterceptor(loginInterceptor)
- .addPathPatterns("/**")
- .excludePathPatterns("/login").
- order(1);
-
- }
- }
全局异常处理器,它能捕获到全部的异常(前提是要把异常抛出),所有异常都会在controller层反应出来,因为执行方法的原头在controller。我之前就是用try-catch处理,然后一直一直捕获不到,然后网上说正常的实现的全局异常器只能捕获到controller层的异常,所以拦截器里的异常捕获不到,这句话对也不对。拦截器里异常确实捕获不到,但只要咱把它抛出去就能再controller层显现了。
具体实现就是我们要加@ControllerAdvice注解, @ExceptionHandler根据这个注解具体绑定处理哪个异常。
- /**
- * 全局异常处理器
- * @author MI
- * @date 2023/10/02
- */
- @Slf4j
- @ControllerAdvice
- public class GlobalExceptionHandler{
- /**
- * 自定义异常拦截器
- * @param req
- * @param e
- * @return {@link Result}
- */
- @ResponseBody
- @ExceptionHandler(value =ServiceException.class)
- public Result exceptionHandler(HttpServletRequest req, ServiceException e){
- log.info("发送{}异常",e.getMessage());
- return Result.getBusinessException(e.getLocalizedMessage(),e.getCode());
- }
-
-
- @ResponseBody
- @ExceptionHandler(value =Exception.class)
- public Result exceptionHandler(HttpServletRequest req, Exception e){
- log.info("发送{}异常",e.getMessage());
- return Result.getBusinessException(e.getLocalizedMessage());
- }
- }
Server层实现:
- @Service
- @Slf4j
- public class LoginServiceImpl implements ILoginService {
-
- @Resource
- UserMapper userMapper;
- @Resource
- TokenUtil tokenUtil;
- @Resource
- UserRoleMapper userRoleMapper;
-
- /**
- * 登录实现
- * @param loginDto
- * @return {@link LoginVo}
- */
- @Override
- public LoginVo login(LoginDto loginDto) {
- UserPo userPo = userMapper.selectOne(new LambdaQueryWrapper<UserPo>().eq(UserPo::getAccount, loginDto.getAccount()));
- if (userPo!=null){
- if (userPo.getPassword().equals(loginDto.getPassword())){
- /**
- * 构建token
- */
- String token = tokenUtil.buildToken(userPo.getAccount());
- LoginVo loginVo = new LoginVo();
- loginVo.setToken(token);
- loginVo.setRoleId(userRoleMapper.selectOne(
- new LambdaQueryWrapper<UserRolePo>().eq(UserRolePo::getUserId,userPo.getUserId())).getRoleId());
- loginVo.setAccount(userPo.getAccount());
- return loginVo;
- }else{
- throw new ServiceException("422","密码错误");
- }
- }else {
- throw new ServiceException("422", "用户不存在");
- }
- }
- }
后面的Controller、Mapper、实体层大家要根据自己的需求字段来进行具体实现,我就不贴出来了。
具体用法,像这个异常咱直接抛出来就行,全局异常处理类都能捕获,就不会继续往下执行了,它会把报错信息返回给前端:
如图所示:
