• Spring Boot Security JWT 整合实现前后端分离认证示例


    前面两章节我们介绍了 Spring Boot Security 快速入门Spring Boot JWT 快速入门,本章节使用 JWT 和 Spring Boot Security 构件一个前后端分离的认证系统。本章代码实例来自于 Spring Boot Security + JWT Hello World Example

    本章节没有采用 thymeleaf,直接采用纯 html 与 rest api 来实现。

    • spring boot security
    • jsonwebtoken
    • jquery 1.11 +

    几个逻辑

    在编写代码前,我们应该搞清楚几个逻辑

    • JWT 认证逻辑是什么?

      JWT认证逻辑见图1,JWT就是向每个请求发送带有 token 的字符串,服务端每次都对每个请求进行拦截认证的过程,成功则放行,失败则抛出异常。

      图1
      Json Web Token 登录验证与授权验证流程

    • JWT 不是认证吗,什么还要 Spring Security

      JWT 作为一种 client-server 即客户端到服务端的认证,是无状态的,但在服务端我们需要有状态的判断,那么就要用到 shiro 或者 spring security 来进行安全管理状态管理。

    • JWT 什么时候需要单独认证,什么时候需要 Spring Security 一起认证

      只要 client 发起请求,我们都需要对 Jwt token 进行认证。在 server 侧,当我们需要进行授权的时候,则需要检测是否授权,需要用到 spring security 认证。

    • JWT 认证有效,Spring Security 认证无效 会出现这种情况吗

      这种情况,则会 在Spring Security 重新登录授权。

    • 本章的业务逻辑

      图2、图3 显示了本章的逻辑

    图2
    JWT 用户登录获取Token详细页面流程图

    图3
    JWT 请求授权页面流程

    本项目源码下载

    1 新建 Spring Boot Maven 示例工程项目

    1. File > New > Project,如下图选择 Spring Initializr 然后点击 【Next】下一步
    2. 填写 GroupId(包名)、Artifact(项目名) 即可。点击 下一步
      groupId=com.fishpro
      artifactId=securityjwt
    3. 选择依赖 Spring Web Starter 前面打钩。
    4. 项目名设置为 spring-boot-study-securityjwt.

    2 依赖引入 Pom.xml

    本文引入了

    • Spring Boot Security
    • jsonwebtoken
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.6.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.fishpro</groupId>
        <artifactId>securityjwt</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>securityjwt</name>
        <description>Demo project for Spring Boot</description>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.1</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
    

    3 配置文件 application

    配置了端口和 jwt 的秘钥

    server:
      port: 8086
    jwt:
     #jwt 的秘钥
      secret: javainuse
    

    4 建立一个正常的 HelloController

    package com.fishpro.securityjwt.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class HelloWorldController {
    
        @RequestMapping({ "/hello" })
        public String firstPage() {
            return "Hello World";
        }
    
    }
    

    此时,访问 localhost:8086/hello 是正常显示,因为此时没有权限要求

    5 建立 Jwt 请求与返回实体

    5.1 JwtRequest 请求类

    package com.fishpro.securityjwt.dto;
    
    import java.io.Serializable;
    
    public class JwtRequest implements Serializable {
    
        private static final long serialVersionUID = 5926468583005150707L;
    
        private String username;
        private String password;
    
        //need default constructor for JSON Parsing
        public JwtRequest()
        {
    
        }
    
        public JwtRequest(String username, String password) {
            this.setUsername(username);
            this.setPassword(password);
        }
    
        public String getUsername() {
            return this.username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return this.password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    }
    

    5.2 JwtResponse 返回类

    package com.fishpro.securityjwt.dto;
    
    import java.io.Serializable;
    
    public class JwtResponse implements Serializable {
    
        private static final long serialVersionUID = -8091879091924046844L;
        private final String jwttoken;
    
        public JwtResponse(String jwttoken) {
            this.jwttoken = jwttoken;
        }
    
        public String getToken() {
            return this.jwttoken;
        }
    }
    
    

    5.3 JwtUtil 操作类

    package com.fishpro.securityjwt.util;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    
    import java.io.Serializable;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.function.Function;
    
    /**
     * jwt 库
     * */
    @Component
    public class JwtTokenUtil implements Serializable {
    
        private static final long serialVersionUID = -2550185165626007488L;
    
        public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
    
        @Value("${jwt.secret}")
        private String secret;
    
        //retrieve username from jwt token
        public String getUsernameFromToken(String token) {
            return getClaimFromToken(token, Claims::getSubject);
        }
    
        //retrieve expiration date from jwt token
        public Date getExpirationDateFromToken(String token) {
            return getClaimFromToken(token, Claims::getExpiration);
        }
    
        public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
            final Claims claims = getAllClaimsFromToken(token);
            return claimsResolver.apply(claims);
        }
        //for retrieveing any information from token we will need the secret key
        private Claims getAllClaimsFromToken(String token) {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        }
    
        //check if the token has expired
        private Boolean isTokenExpired(String token) {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        }
    
        //generate token for user
        public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            return doGenerateToken(claims, userDetails.getUsername());
        }
    
        //while creating the token -
        //1. Define  claims of the token, like Issuer, Expiration, Subject, and the ID
        //2. Sign the JWT using the HS512 algorithm and secret key.
        //3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
        //   compaction of the JWT to a URL-safe string
        private String doGenerateToken(Map<String, Object> claims, String subject) {
    
            return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                    .signWith(SignatureAlgorithm.HS512, secret).compact();
        }
    
        //validate token
        public Boolean validateToken(String token, UserDetails userDetails) {
            final String username = getUsernameFromToken(token);
            return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
        }
    }
    
    

    5.4 JwtUserDetailsService

    package com.fishpro.securityjwt.config;
    
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    
    @Service
    public class JwtUserDetailsService implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            if ("javainuse".equals(username)) {
                return new User("javainuse", "$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6",
                        new ArrayList<>());
            } else {
                throw new UsernameNotFoundException("User not found with username: " + username);
            }
        }
    }
    
    

    6 重新定义 AuthenticationEntryPoint 页面未授权统一返回

    用来解决匿名用户访问无权限资源时的异常

    package com.fishpro.securityjwt.config;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.Serializable;
    
    /**
     * AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
     * AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常
     * */
    @Component
    public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
    
        private static final long serialVersionUID = -7858869558953243875L;
    
        //当出错的时候 发送 Unauthorized
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
    
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
        }
    }
    
    

    7 JwtRequestFilter 过滤器用于验证 Jwt

    package com.fishpro.securityjwt.config;
    
    import com.fishpro.securityjwt.util.JwtTokenUtil;
    import io.jsonwebtoken.ExpiredJwtException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * 过滤器 用于 Spring Boot Security
     * OncePerRequestFilter 一次请求只通过一次filter,而不需要重复执行
     * */
    @Component
    public class JwtRequestFilter extends OncePerRequestFilter {
    
        @Autowired
        private JwtUserDetailsService jwtUserDetailsService;
    
        @Autowired
        private JwtTokenUtil jwtTokenUtil;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
    
            final String requestTokenHeader = request.getHeader("Authorization");
    
            String username = null;
            String jwtToken = null;
            // JWT Token 获取请求头部的 Bearer
            // only the Token
            if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
                jwtToken = requestTokenHeader.substring(7);
                try {
                    username = jwtTokenUtil.getUsernameFromToken(jwtToken);
                } catch (IllegalArgumentException e) {
                    System.out.println("Unable to get JWT Token");
                } catch (ExpiredJwtException e) {
                    System.out.println("JWT Token has expired");
                }
            } else {
                logger.warn("JWT Token does not begin with Bearer String");
            }
    
            // 验证
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
    
                UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
    
                // JWT 验证通过 使用Spring Security 管理
                if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
    
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    usernamePasswordAuthenticationToken
                            .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    // After setting the Authentication in the context, we specify
                    // that the current user is authenticated. So it passes the
                    // Spring Security Configurations successfully.
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            }
            chain.doFilter(request, response);
        }
    
    }
    

    8 定义用于验证 Jwt Token 的路由

    package com.fishpro.securityjwt.config;
    
    import com.fishpro.securityjwt.dto.JwtRequest;
    import com.fishpro.securityjwt.dto.JwtResponse;
    import com.fishpro.securityjwt.util.JwtTokenUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.DisabledException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * 用于验证 jwt 返回客户端 jwt(json web token)
     * */
    @RestController
    @CrossOrigin
    public class JwtAuthenticationController {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private JwtTokenUtil jwtTokenUtil;
    
        @Autowired
        private JwtUserDetailsService userDetailsService;
    
        /**
         * 获取 客户端来的 username password 使用秘钥加密成 json web token
         * */
        @RequestMapping(value = "/authenticate", method = RequestMethod.POST)
        public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
    
            authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
    
            final UserDetails userDetails = userDetailsService
                    .loadUserByUsername(authenticationRequest.getUsername());
    
            final String token = jwtTokenUtil.generateToken(userDetails);
    
            return ResponseEntity.ok(new JwtResponse(token));
        }
    
        /**
         *  获取 客户端来的 username password 使用秘钥加密成 json web token
         * */
        private void authenticate(String username, String password) throws Exception {
            try {
                authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
            } catch (DisabledException e) {
                throw new Exception("USER_DISABLED", e);
            } catch (BadCredentialsException e) {
                throw new Exception("INVALID_CREDENTIALS", e);
            }
        }
    }
    

    9 定义 WebSecurityConfig

    
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    
        @Autowired
        private UserDetailsService jwtUserDetailsService;
    
        @Autowired
        private JwtRequestFilter jwtRequestFilter;
    
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            // configure AuthenticationManager so that it knows from where to load
            // user for matching credentials
            // Use BCryptPasswordEncoder
            auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
            // 本示例不需要使用CSRF
            httpSecurity.csrf().disable()
                    // 认证页面不需要权限
                    .authorizeRequests().antMatchers("/authenticate").permitAll().
                    //其他页面
                            anyRequest().authenticated().and().
                    //登录页面 模拟客户端
                    formLogin().loginPage("/login.html").permitAll().and().
                    // store user's state.
                     exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
                    //不使用session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
            //验证请求是否正确
            httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }
    

    10 login.html 模拟客户端

    注意这里使用 ajax 的时候务必填写参数 contentType: "application/json;charset=UTF-8"

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>使用 jwt 登录页面</title>
    </head>
    <body>
    <div>
        <input id="userName" name="userName" value="">
    </div>
    <div>
        <input id="password" name="password" value="">
    </div>
    <div>
        <input type="button" id="btnSave"  value="登录">
    </div>
    <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.js"></script>
    <script>
        $(function() {
            $("#btnSave").click(function () {
                var username=$("#userName").val();
                var password=$("#password").val();
                $.ajax({
                    cache: true,
                    type: "POST",
                    url: "/authenticate",
                    contentType: "application/json;charset=UTF-8",
                    data:JSON.stringify({"username":username ,"password" : password}),
                    dataType: "json",
                    async: false,
                    error: function (request) {
                        console.log("Connection error");
                    },
                    success: function (data) {
                        //save token
                        localStorage.setItem("token",data.token);
    
    
                    }
                });
            });
        });
    </script>
    </body>
    </html>
    

    本项目源码下载

  • 相关阅读:
    关于敏捷软件开发的一些感悟
    求出矩阵中,所有元素相加和最大的分块矩阵。
    小组作业提交报告
    结对项目实训——电梯调度
    关于代码测试方面的一些想法和感悟
    用c语言实现文本文件中的字符筛选分析(二)
    用c语言实现文本文件中的字符筛选分析(一)
    4月19日会议(整理——郑云飞)
    4月18日会议总结(整理—祁子梁)
    每日任务看板展示—第一周
  • 原文地址:https://www.cnblogs.com/fishpro/p/spring-boot-study-securing-jwt.html
Copyright © 2020-2023  润新知