开发者

springboot security使用jwt认证方式

目录
  • 前言
  • 代码示例
    • 依赖
    • 定义mapper
    • 定义用户信息的实体bean
    • security相关的类
    • 提供登录接口
  • 测试
    • 提供一个用于测试的接口
    • 验证
  • 总结

    前言

    在前面的几篇文章中:

    • spring boot security快速使用示例 
    • spring boot security之前后端分离配置 
    • spring boot security自定义认证 
    • spring boot security验证码登录示例

    基本对常用的基于cookie和session的认证使用场景都已覆盖。但是session属于有状态认证,本文给出一个无状态的认证:jwt认证示例。

    代码示例

    下面会提供完整的示例代码:

    依赖

    使用的spring boot 2.6.11版本,jdk8。

    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-security</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.projectlombok</groupId>
    			<artifactId>lombok</artifactId>
    			<version>1.18.20</version>
    			<scope>provided</scope>
    		</dependency>
    
    		<dependency>
    			<groupId>com.github.penggle</groupId>
    			<artifactId>kaptcha</artifactId>
    			<version>2.3.2</version>
    		</dependency>
    
    		<dependency>
    			<groupId>io.jsonwebtoken</groupId>
    			<artifactId>jjwt</artifactId>
    			<version>0.9.1</version编程客栈>
    		</dependency>

    定义mapper

    定义一个查询用户信息的接口:

    @Component
    public class UserMapper {
    
       public User select(String username) {
            return new User(username, "pass");
        }
    }

    定义用户信息的实体bean

    @Data
    public class User {
    
        private String username;
    
        private String password;
    
        private String captcha;
    
        public User() {
        }
    
        public User(String username, String password) {
            this.username = username;
            this.password = password;
        }
    
        public User(String username, String password, String captcha) {
            this.username = username;
            this.password = password;
            this.captcha = captcha;
        }
    }

    security相关的类

    1. 实现spring security内置的UserDetailsService接口,根据用户名返回用户信息:
    @Slf4j
    @Component
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        public static final UserDetails INVALID_USER =
                new org.springframework.security.core.userdetails.User("invalid_user", "invalid_password", Collections.emptyList());
    
        private final UserMapper userMapper;
    
        public UserDetailsServiceImpl(UserMapper userMapper) {
            this.userMapper = userMapper;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 根据用户名从数据库查询用户信息
            User user = userMapper.select(username);
            if (user == null) {
                /**
                 * 如果没查询到这个用户,考虑两种选择:
                 * 1. 返回一个标记无效用户的常量对象
                 * 2. 返回一个不可能认证通过的用户
                 */
                return INVALID_USER;
    //            return new User(username, System.currentTimeMillis() + UUID.randomUUID().toString(), Collections.emptyList());
            }
            /**
             * 这里返回的用户密码是否为库里保存的密码,是明文/密文,取决于认证时密码比对部分的实现,每个人的场景不一样,
             * 因为使用的是不加密的PasswordEncoder,所以可以返回明文
             */
            return new org.springframework.security.core.userdetails.User(username, user.getPassword(), Collections.emptyList());
        }
    }
    1. 定义jwt工具类
    public class JwtUtil {
    
        public static final String SECRET = TextCodec.BASE64.encode("secret");
    
        public static final long EXPIRE_SECONDS = 3600L;
    
        /**
         * 从token中解析出用户名
         */
        public static String getUsernameFromToken(String token) {
            return getClaimFromToken(token, Claims::get编程Subject);
        }
    
        /**
         * 从token中获取过期时间
         */
        public static Date getExpirationDateFromToken(String token) {
            return getClaimFromToken(token, Claims::getExpiration);
        }
    
        /**
         * 解析出token声明.
         */
        public static <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
            final Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
            return claimsResolver.apply(claims);
        }
    
        /**
         * token是否过期
         */
        public static Boolean isTokenExpired(String token) {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        }
    
        /**
         * 生成token
         */
        public static String generateToken(UserDetails userDetails) {
            jsMap<String, Object> claims = new HashMap<>();
            return doGenerateToken(claims, userDetails.getUsername());
        }
    
        /**
         * token是否合法.
         */
        public static Boolean isValidateToken(String token, UserDetails userDetails) {
            final String username = getUsernameFromToken(token);
            return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
        }
    
        private static String doGenerateToken(Map<String, Object> claims, String subject) {
            return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMiphpllis() + EXPIRE_SECONDS * 1000))
                    .signWith(SignatureAlgorithm.HS512, SECRET).compact();
        }
    
    }
    1. 定义jwt认证的过滤器
    @Slf4j
    @Component
    public class JwtRequestFilter extends OncePerRequestFilter {
    
    
        private final UserDetailsService userDetailsService;
    
    
        public JwtRequestFilter(UserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
    
            final String requestTokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
    
            String username = null;
            String jwtToken = null;
            if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
                jwtToken = requestTokenHeader.substring(7);
                try {
                    username = JwtUtil.getUsernameFromToken(jwtToken);
                } catch (Exception e) {
                    log.error("获取token失败: {}, {}", jwtToken, e.getMessage());
                }
            }
    
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                // 根据用户名加载用户信息
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                // 判断token是否有效
                if (JwtUtil.isValidateToken(jwtToken, userDetails)) {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    usernamePasswordAuthenticationToken
                            .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            }
            chain.doFilter(request, response);
        }
    
    }
    1. 注册相关bean到spring容器
    @Configuration
    public class WebConfiguration {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            // 示例,不对密码进行加密处理
            return NoOpPasswordEncoder.getInstance();
        }
    
    
        @Bean
        public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
            DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
            // 设置加载用户信息的类
            provider.setUserDetailsService(userDetailsService);
            // 比较用户密码的时候,密码加密方式
            provider.setPasswordEncoder(passwordEncoder);
            return new ProviderManager(Arrays.asList(provider));
        }
        
        @Bean
        public Producer defaultKaptcha() {
            Properties properties = new Properties();
            // 还有一些其它属性,可以进行源码自己看相关配置,比较清楚了,根据变量名也能猜出来什么意思了
            properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "150");
            properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "50");
            properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789abcdefghigklmnopqrstuvwxyz");
            properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
            Config config = new Config(properties);
            DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
            defaultKaptcha.setConfig(config);
            return defaultKaptcha;
        }
    
    }
    1. 自定义 WebSecurityConfigurer
    @Component
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        private final JwtRequestFilter jwtRequestFilter;
    
        public WebSecurityConfigurer(JwtRequestFilter jwtRequestFilter) {
            this.jwtRequestFilter = jwtRequestFilter;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 在这里自定义配置
            http.authorizeRequests()
                    // 登录相关接口都允许访问
                    .antMatchers("/login/**").permitAll()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .exceptionHandling()
                    // 认证失败返回401状态码,前端页面可以根据401状态码跳转到登录页面
                    .authenticationEntryPoint((request, response, authException) ->
                            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()))
                    .and().cors()
                    // csrf是否决定禁用,请自行考量
                    .and().csrf().disable()
                    // 采用http 的基本认证.
                    .httpBasic()
                    // 设置session是无关的
                    .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and().addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    
        }
    }

    提供登录接口

    @RequestMapping("/login")
    @RestController
    public class LoginController {
    
        private final AuthenticationManager authenticationManager;
    
        private final Producer producer;
    
        public LoginController(Authenticat编程ionManager authenticationManager, Producer producer) {
            this.authenticationManager = authenticationManager;
            this.producer = producer;
        }
    
        @PostMapping()
        public Object login(@RequestBody User user, HttpSession session) {
            Object captcha = session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
            if (captcha == null || !captcha.toString().equalsIgnoreCase(user.getCaptcha())) {
                return "captcha is not correct.";
            }
            try {
                // 使用定义的AuthenticationManager进行认证处理
                Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
                // 认证通过,设置到当前上下文,如果当前认证过程后续还有处理的逻辑需要的话。这个示例是没有必要了
                SecurityContextHolder.getContext().setAuthentication(authenticate);
                String token = JwtUtil.generateToken((UserDetails) authenticate.getPrincipal());
                return token;
            } catch (Exception e) {
                return "login failed";
            }
        }
    
        /**
         * 获取验证码,需要的话,可以提供一个验证码获取的接口,在上面的login里把验证码传进来进行比对
         */
        @GetMapping("/captcha")
        public void captcha(HttpServletResponse response, HttpSession session) throws IOException {
            response.setContentType("image/jpeg");
            String text = producer.createText();
            session.setAttribute(Constants.KAPTCHA_SESSION_KEY, text);
            BufferedImage image = producer.createImage(text);
            try (ServletOutputStream out = response.getOutputStream()) {
                ImageIO.write(image, "jpg", out);
            }
        }
    }

    测试

    提供一个用于测试的接口

    @RequestMapping("/hello")
    @RestController
    public class HelloController {
    
        @GetMapping("/world")
        public Object helloWorld() {
            return "hello, world";
        }
    }

    验证

    • 获取验证码

    springboot security使用jwt认证方式

    • 登录

    springboot security使用jwt认证方式

    • 使用登录的token访问接口

    springboot security使用jwt认证方式

    • 如果没有token或不正确是访问受限的

    springboot security使用jwt认证方式

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜