抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

什么是JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

传统Cookie+Session与JWT对比

① 在传统的用户登录认证中,因为http是无状态的,所以都是采用session方式。用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。cookie+session这种模式通常是保存在内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。而JWT不是这样的,只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。

② JWT方式校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录,验证token更为简单。

项目搭建

spring boot + Security + JWT + JPA。说明我已经全部写在注解里。

目录结构

https://img-blog.csdnimg.cn/20200702145345204.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2MzU3MjQy,size_16,color_FFFFFF,t_70

引入jar包

<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>

    <groupId>com.gbq.jpa.jwt.demo</groupId>
    <artifactId>jpa-jwt-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </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>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
 
        <!-- 引入jpa 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
 
        <!-- JWT依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.32</version>
        </dependency>
 
    </dependencies>
 
    <!-- 使用maven打包 -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

yml配置

server:
  tomcat:
    uri-encoding: UTF-8
  port: 8080
 
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
 
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
 
jwt:
  secret: secret
  expiration: 7200000
  token: Authorization

bean

@Entity
@Table(name = "user")
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
 
    @Column(nullable = false)
    private String username;
 
    @Column(nullable = false)
    private String password;
 
}

dao

public interface UserDao extends JpaRepository<User, Integer> {
 
    //自定义repository。手写sql
    @Query(value = "update user set name=?1 where id=?2",nativeQuery = true)   //占位符传值形式
    @Modifying
    int updateById(String name,int id);
 
    @Query("from User u where u.username=:username")   //SPEL表达式
    User findUser(@Param("username") String username);// 参数username 映射到数据库字段username
}

service

 *  Created by 阿前
 *  2020年6月30日15:56:36
 */
public interface UserService {
 
    User getUser(String loginName);
}

serviceImpl

public class UserServiceImpl implements UserService {
 
    @Resource
    private UserDao userDao;
 
    @Override
    public User getUser(String username) {
        return userDao.findUser(username);
    }
}

security配置

@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class SecurityUserDetails extends User implements UserDetails {
 
    private Collection<? extends GrantedAuthority> authorities;
 
    public SecurityUserDetails(User user) {
        if (user != null) {
            this.setUsername(user.getUsername());
            this.setPassword(user.getPassword());
        }
    }
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        String username = this.getUsername();
        if (username != null) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(username);
            authorities.add(authority);//分配权限
        }
        return authorities;
    }
 
 
    /**
     * 账户是否过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    /**
     * 是否禁用
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    /**
     * 密码是否过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    /**
     * 是否启用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

comment(JWT+Security验证)

//jwt验证
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
 
    private final UserDetailsService userDetailsService;
    private final JwtTokenComment jwtTokenComment;
    private final String tokenHeader;
 
    public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsService") UserDetailsService userDetailsService,
                                       JwtTokenComment jwtTokenComment, @Value("${jwt.token}") String tokenHeader) {
        this.userDetailsService = userDetailsService;
        this.jwtTokenComment = jwtTokenComment;
        this.tokenHeader = tokenHeader;
    }
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        final String requestHeader = request.getHeader(this.tokenHeader);
        String username = null;
        String authToken = null;
        if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
            authToken = requestHeader.substring(7);
            try {
                username = jwtTokenComment.getUsernameFromToken(authToken);
            } catch (ExpiredJwtException e) {
            }
        }
 
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
 
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
 
            if (jwtTokenComment.validateToken(authToken, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
 
        }
        chain.doFilter(request, response);
    }
 
}

@Component
public class JwtUserDetailsService implements UserDetailsService {

    @Resource
    private UserService userService;
 
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        System.out.println("JwtUserDetailsService:" + s);
        User user = userService.getUser(s);
        if (user == null) throw new UsernameNotFoundException("Username " + s + " not found");
        return new SecurityUserDetails(user);
    }

}

@Component
public class LoadUserComment {
 
    @Resource
    private UserDetailsService userDetailsService;
 
    public UserDetails loadUserByUsername(String username, String password) throws BusinessException {
        try {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (userDetails != null) {
                if (!userDetails.getPassword().contains(password)) {
                    throw new BackingStoreException("密码不正确");
                } else {
                    return userDetails;
                }
            }else {
                throw new BackingStoreException("用户不存在");
            }
        } catch (BackingStoreException e) {
            throw new BusinessException(e.getMessage());
        }
    }
}

@Component
public class JwtTokenComment {

    private static final long serialVersionUID = -3301605591108950415L;
 
    @Value("${jwt.secret}")
    private  String secret;
 
    @Value("${jwt.expiration}")
    private Long expiration;
 
    @Value("${jwt.token}")
    private String tokenHeader;
 
    private Clock clock = DefaultClock.INSTANCE;
 
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }
 
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);
 
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
 
    private Date calculateExpirationDate(Date createdDate) {
        return new Date(createdDate.getTime() + expiration);
    }
 
    public Boolean validateToken(String token, UserDetails userDetails) {
        SecurityUserDetails user = (SecurityUserDetails) userDetails;
        final String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername())
                && !isTokenExpired(token)
        );
    }
 
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
 
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }
 
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }
 
 
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(clock.now());
    }
 
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }
}

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException)
            throws IOException, ServletException {
 
        System.out.println("JwtAuthenticationEntryPoint:"+authException.getMessage());
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"没有凭证");
    }
}

controller

@RestController
@Slf4j
public class UserController {
 
    @Resource
    private LoadUserComment loadUserComment;
 
    @Resource
    @Qualifier
    private JwtTokenComment jwtTokenComment;
 
 
    @PostMapping("login")
    public HashMap<String, Object> login (@RequestBody Map<String,String> map){
 
        HashMap<String, Object> result = new HashMap<>();
        String username = map.get("username");
        String password = map.get("password");
 
        UserDetails userDetails = loadUserComment.loadUserByUsername(username,password);
        String token = jwtTokenComment.generateToken(userDetails);
 
        result.put("token",token);
        result.put("user",userDetails);
        return result;
    }
 
    @GetMapping("getUser")
    public String getUser(){
        UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return "getUser:"+userDetails.getUsername()+","+userDetails.getPassword();
    }
}

测试

https://img-blog.csdnimg.cn/20200702154622307.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2MzU3MjQy,size_16,color_FFFFFF,t_70

https://img-blog.csdnimg.cn/20200702154644198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2MzU3MjQy,size_16,color_FFFFFF,t_70

评论