单点登录一:springboot项目使用redis、springSecurity、jwt实现单点登录的demo

1、jwt原理

JWT(JSON Web Token)是一种用于身份验证和授权的开放标准(RFC 7519)。它是一种轻量级的令牌格式,用于在网络应用间传输声明(claims)。JWT使用JSON对象来编码声明,并使用数字签名(或加密)以确保声明的完整性和安全性。

1.1、内容:

内容包含三个部分:头部(Header)、载荷(Payload)和签名(Signature):

  • 头部(Header):

头部通常由两部分组成:令牌的类型(typ)和使用的签名算法(alg)。常见的签名算法包括 HMAC SHA256(HS256)、RSA SHA256(RS256)等。头部会通过Base64编码后放置在JWT的第一个部分。

  • 载荷(Payload):

载荷包含了一组声明(claims),用于携带关于用户和其他实体的信息。有三种类型的声明:

Registered Claims(注册声明):这些是预定义的声明,包含一些标准字段,如iss(签发者)、sub(主题)、exp(过期时间)、aud(受众)等。

Public Claims(公共声明):这些是自定义的声明,可以根据应用程序的需求添加任意的字段。

Private Claims(私有声明):这些是自定义的声明,用于在双方之间共享信息,但不建议在JWT中公开传输。

载荷会被Base64编码后放置在JWT的第二个部分。

  • 签名(Signature):

签名是对头部和载荷进行签名的结果,以确保JWT的完整性和真实性。签名通常使用头部中指定的算法和密钥进行计算。签名可以防止未经授权的修改和篡改JWT。签名是JWT的第三个部分。

完整的JWT由这三个部分通过点号(.)连接而成,形如:xxxxx.yyyyy.zzzzz。

2、springBoot项目使用springSecurity框架实现单点登录demo

2.1、创建一个最基本的springBoot项目(此处略),然后引入springSecurity、redis、jwt相关依赖

 <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
<!--redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
<!--spring security-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
<!--jwt的依赖-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.2</version>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.2</version>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.2</version>
        <scope>runtime</scope>
    </dependency>

2.2、配置类及工具类代码实现

jwt工具类

package com.loong.nba.player.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @author 
 * @date 2023/5/18
 */
@Component
public class JwtUtil {

    private final Key secretKey;
    private final long expirationTime;

    @Autowired
    private StringRedisTemplate redisTemplate;

    public JwtUtil() {
        
        //这里我是用随机生成的,兄弟们也可以手动写死一个
        this.secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        this.expirationTime = 1200000;

    }

    /**
     * 生成jwt令牌
     *
     * @param username 用户名
     * @return token
     */
    public String generateToken(String username) {
        Date now = new Date();
        Date expirationDate = new Date(System.currentTimeMillis() + expirationTime);
        String token = Jwts.builder()
                .setSubject(username)
                .setIssuedAt(now)
                .setExpiration(expirationDate)
                .signWith(secretKey, SignatureAlgorithm.HS256)
                .compact();
        redisTemplate.opsForValue().set(username, token, expirationTime, TimeUnit.MILLISECONDS);

        return token;
    }

    /**
     * 解析令牌
     *
     * @param token token
     * @return 内容
     */
    public String getUsernameFormToken(String token) {
        return extractClaims(token).getBody().getSubject();
    }

// 验证token
    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = extractClaims(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    private Jws<Claims> extractClaims(String token) {
        JwtParser parser = Jwts.parserBuilder().setSigningKey(secretKey).build();
        return parser.parseClaimsJws(token);
    }
}

自定义Filter类

package com.loong.nba.player.filter;

import com.loong.nba.player.util.JwtUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
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;
import java.util.Collections;

/**
 * @author 
 * @date 2023/5/18
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final String tokenHeader = "Authorization";
    private final String tokenPrefix = "Bearer ";

    private final JwtUtil jwtUtil;
    private final StringRedisTemplate redisTemplate;

    public JwtAuthenticationFilter(JwtUtil jwtUtil, StringRedisTemplate redisTemplate) {

        this.jwtUtil = jwtUtil;
        this.redisTemplate = redisTemplate;
    }


    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
      //从请求头拿到token
          String header = request.getHeader(tokenHeader);
        if (!ObjectUtils.isEmpty(header) && header.startsWith(tokenPrefix)) {
            //去掉头部的字符串
            String token = header.replace(tokenPrefix, "");
            if (!ObjectUtils.isEmpty(token) && jwtUtil.validateToken(token)) {
                
                //解析token,拿到usename
                String username = jwtUtil.getUsernameFormToken(token);
                
                //再从redis中取出对应jwt(不存在有可能是token有问题,还有就是redis里面token已经过期了)
                if (redisTemplate.opsForValue().get(username) != null) {
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                            username, null, Collections.emptyList());
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

springSecruity配置类

package com.loong.nba.player.config;

import com.loong.nba.player.filter.JwtAuthenticationFilter;
import com.loong.nba.player.util.JwtUtil;
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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @author
 * @date 2023/5/18
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        //这里是允许登录接口通过,不用校验。以及后续关联我们自定义的filter类
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

2.3、业务代码

实体类

最简单的user

public class UserDO {
    private String name;
    private String password;

    public UserDO() {
    }

    public UserDO(String name, String password) {
        this.name = name;
        this.password = password;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

Controller

package com.loong.nba.player.controller;

import com.loong.nba.player.pojo.UserDO;
import com.loong.nba.player.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;

/**
 * @author
 * @date 2023/5/15
 */
@RestController
public class LoginController {

    private final JwtUtil jwtUtil;

    public LoginController(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }


    @PostMapping("/login")
    public ResponseEntity<UserDO> postLogin(@RequestBody UserDO userDO, HttpServletResponse response) {
        //查询持久层代码以及校验密码部分这里不写了,直接假数据定义
        //正常应该是前端拿到的密文,通过后端一系列的解密加验证密码,才能通过,因为只演示框架功能,这里逻辑代码就不写了
        String username = "admin";

        String token = jwtUtil.generateToken(username);
        response.addHeader("Authorization", "Bearer " + token);
        return ResponseEntity.ok(userDO);
    }

    @GetMapping("/test")
    public String test(){
        return "hello test";
    }
}

2.4 测试

代码可以直接运行,跑起来以后,请求登录接口,从response中拿到headers中Key为Authorization的值,就是token

然后再请求别的接口,如代码中的test接口,和登录接口不一样,它一定要在请求头中添加上Authorization这个键值对,不然会报403异常。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿星_Alex

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值