springboot实现登录demo

实现简单的登录功能

实体类

定义实体类为User3类。
使用@Data:提供类的get,set,equals,hashCode,canEqual,toString方法;
使用@AllArgsConstructor:提供类的全参构造
使用@NoArgsConstructor:提供类的无参构造
类代码如下

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User3
{
    private Integer id;
    private String userName;
    private String password;
}

JWT核心类

JWT认证流程如图所示
img
可见服务器端需要做两件事,一是根据用户信息创建对应的JWT密钥,另一个是根据对应的密钥解码用户信息。

JWT token主要由三个部分组成,主要包括Header,Payload,Signature。通过“."间隔开来。

  • Header:包括两部分信息,令牌的类型(typ),签名算法(alg)。
  • Payload:也称为声明(Claims),包含JWT的主要信息,主要是用户身份信息,权限等,是一个JSON对象。
  • Signature:使用头部使用的算法和密钥对头部和载荷进行签名所生成的一部分,用来验证真实性和完整性。

签名主要是先将头部和载荷转换为Json格式,并进行base64编码,最后用.拼接在一起

Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "1234567890", "name": "John Doe", "admin": true}

转换后为

Encoded Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Encoded Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

使用指定的算法对Encoded Header.Payload进行签名。最后再拼接在一起
验证时使用JWTVerifier实例,使用 HMAC256 算法和指定的密钥(SECRET)来验证签名。
主要代码如下

public class JwtUtil {
    private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
    /**
     * 密钥
     */
    private static final String SECRET = "my_secret";
    /**
     * 过期时间
     **/
    private static final long EXPIRATION = 1800L;//单位为秒
    /**
     * 生成用户token,设置token超时时间
     */
    public static String createToken(User3 user) {
        //过期时间30分钟
        Date expireDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
        Map<String, Object> map = new HashMap<>();
        map.put("alg", "HS256");
        map.put("typ", "JWT");
        String token = JWT.create()
                .withHeader(map)// 添加头部
                //可以将基本信息放到claims中
                .withClaim("id", user.getId())//userId
                .withClaim("userName", user.getUserName())//userName
                .withClaim("password", user.getPassword())//password
                .withExpiresAt(expireDate) //超时设置,设置过期的日期
                .withIssuedAt(new Date()) //签发时间
                .sign(Algorithm.HMAC256(SECRET)); //SECRET加密
        System.out.println("生成的token:"+token);
        return token;
    }
    /**
     * 校验token并解析token
     */
    public static Map<String, Claim> verifyToken(String token) {
        DecodedJWT jwt = null;
        System.out.println("识别的token:"+token);
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            jwt = verifier.verify(token);
        } catch (Exception e) {
            logger.error(e.getMessage());
            logger.error("token解码异常");
            return null;
        }
        return jwt.getClaims();
    }
}

JWT过滤器

使用过滤器可以提供一种轻量级,安全,高效的方式来处理身份验证和授权,可以简化开发流程。
可以避免服务器每次请求都进行状态管理,无需每次都查询数据库验证用户身份或访问权限。
使用过滤器前我们首先要进行注册

@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new JwtFilter());
        registrationBean.addUrlPatterns("/secure/*");
        return registrationBean;
    }
}

由于用户在每次请求时都会带上Authorization的token,所以要提取出对应的token,识别出用户对应的信息后再进行下一步处理。
整个过程处于过滤器链中,对于 OPTIONS 请求,直接放行并调用 chain.doFilter(request, response);
过滤器的代码如下。

public class JwtFilter implements Filter
{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;
        String temp = request.getHeader("authorization");
        System.out.println("temp:" + temp);
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        final String token = request.getHeader("authorization");
        System.out.println("token:" + token);
        if ("OPTIONS".equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            chain.doFilter(request, response);
        }
        else {

            if (token == null) {
                response.getWriter().write("没有token!");
                return;
            }
            Map<String, Claim> userData = JwtUtil.verifyToken(token);
            if (userData == null) {
                response.getWriter().write("token不合法!");
                return;
            }
            Integer id = userData.get("id").asInt();
            String userName = userData.get("userName").asString();
            String password= userData.get("password").asString();
            //拦截器 拿到用户信息,放到request中
            request.setAttribute("id", id);
            request.setAttribute("userName", userName);
            request.setAttribute("password", password);
            chain.doFilter(req, res);
        }
    }
    @Override
    public void destroy() {
    }
}

或许会有个疑问,那就是这里代码也调用了chain.doFilter(req, res);会导致递归等问题吗。
其实并不会,每个过滤器都会调用chain.doFilter(req, res)方法把请求传递给下一个过滤器,直到最后一个过滤器调用目标Servlet。整个过程处于线性并且有终点。
一般来说,过滤器链的工作流程如下:

  • 过滤器链初始化:当一个请求到达时,服务器根据配置初始化过滤器链。
  • 过滤器链顺序执行:请求进入第一个过滤器,第一个过滤器在逻辑中调用chain.doFilter(request, response); 将请求传递给下一个过滤器。
  • 传递到目标资源:过程一直持续,直到最后一个过滤器调用chain.doFilter(request, response)传递给最终的目标Servlet或JSP页面来处理。
  • 响应返回:目标资源生成响应后,响应会逆向通过所有过滤器回传给客户端,每个过滤器都有机会在返回路径上再次处理响应。

下面以一个例子来说明

public class Filter1 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("Filter1: before chain");
        chain.doFilter(request, response); // 将请求传递给下一个过滤器
        System.out.println("Filter1: after chain");
    }
}

public class Filter2 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("Filter2: before chain");
        chain.doFilter(request, response); // 将请求传递给下一个过滤器
        System.out.println("Filter2: after chain");
    }
}

public class TargetServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.getWriter().write("TargetServlet response");
    }
}

当请求到达时,控制流执行如下:
Filter1 执行前部分代码(Filter1:before chain)。
Filter1 调用 chain.doFilter(request, response); 将请求传递给 Filter2。
Filter2 执行前部分代码(Filter2: before chain)。
Filter2 调用 chain.doFilter(request, response); 将请求传递给 TargetServlet。
TargetServlet 生成响应。
响应传回 Filter2,执行后部分代码(Filter2: after chain)。
响应传回 Filter1,执行后部分代码(Filter1: after chain)。
最终响应传回给客户端。

如果过滤器链中的最后一个过滤器调用了chain.doFilter(request, response);,请求将被传到目标资源。

解码返回响应

最后为了查看解码是否正确,还需要查看解码后的用户信息
编写类代码如下

  @RequestMapping("/secure/getUserInfo")
    @UnInterception
    public JSONObject login(HttpServletRequest request) {
//        final String token = request.getHeader("authorization");
//        System.out.println("token:" + token);
        Integer id = (Integer) request.getAttribute("id");
        System.out.println(id);
        String userName = request.getAttribute("userName").toString();
        String password = request.getAttribute("password").toString();
        HttpServletResponse response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
        assert response != null;
        response.setCharacterEncoding("UTF-8");
        JSONObject test = new JSONObject();
        test.put("id",id);
        test.put("userName",userName);
        test.put("password",password);
        System.out.println("当前用户信息id=" + id + ", userName=" + userName + ", password=" + password);
        return test;
    }

这里的request是已经完成了过滤后的请求,注意要设置字符编码为UTF-8,否则可能会返回乱码。

    static Map<Integer, User3> userMap = new HashMap<>();

    static {

        User3 user1 = new User3(1,"张三","123456");
        userMap.put(1, user1);
        User3 user2 = new User3(2,"李四","123123");
        userMap.put(2, user2);
    }
    /**
     * 模拟用户 登录
     */
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    @UnInterception
    public String login(@RequestParam String userName, @RequestParam String password)
    {
        System.out.println("userName: " + userName + ", password: " + password);
        for (User3 dbUser : userMap.values()) {
            if (dbUser.getUserName().equals(userName) && dbUser.getPassword().equals(password)) {
                log.info("登录成功!生成token!");
                String token = JwtUtil.createToken(dbUser);
                return token;
            }
        }
        return "";
    }

测试结果

在Apifox上测试后结果如下。
img
img
img
可以看到正常发送了请求并且解码成功,而且编码是UTF-8

posted @ 2024-07-05 16:16  Sun-Wind  阅读(57)  评论(0编辑  收藏  举报