Spring boot整合JWT + Security

    技术2022-07-15  85

    什么是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。说明全部写在注解里。

    目录结构

    pom文件

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

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

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

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

    security配置

    securityconfig

    @Data @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; } }

    Security拦截配置

    @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Resource JwtUserDetailsService jwtUserDetailsService; @Resource JwtAuthorizationTokenFilter authenticationTokenFilter; //先来这里认证一下 @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoderBean()); } //拦截在这配 @Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .authorizeRequests() .antMatchers("/login").permitAll() .antMatchers("/haha").permitAll() .antMatchers(HttpMethod.OPTIONS, "/**").anonymous() .anyRequest().authenticated() // 剩下所有的验证都需要验证 .and() .csrf().disable() // 禁用 Spring Security 自带的跨域处理 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 定制我们自己的 session 策略:调整为让 Spring Security 不创建和使用 session http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder passwordEncoderBean() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }

    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(); } }

    测试

     

     

    加入我们

    如果有需要,欢迎可以加入我们的QQ群!(QQ搜索 1074281704, 加入我们的QQ群吧!) 有任何问题,也可以加入我们的QQ群,欢迎交(che)流(dan)!

    Processed: 0.009, SQL: 9