本次基于Spring Boot整合了Spring SecurityJwt,可以解决前后端分离之后用户认证与授权的问题。在前后端还未分离的时候,对用户进行身份认证大约是这样的。



客户端...


服务端...
用户登录
用户登录
登录成功,用户信息存储session
登录成功,用户信息存储session
返回登录状态,并发送cookie
返回登录状态,并发送cookie
将服务端发来的cookie存储
将服务端发来的cookie存储
携带cookie,发送请求
携带cookie,发送请求
Viewer does not support full SVG 1.1

这种缺点就是身份信息需要客户端和服务器同时存储,当用户基数很大的时候,需要大量的内存来解决这个问题。

在前后端分离之后,基于token的用户身份认证大约是这样的。



客户端...


服务端...
用户登录
用户登录
登录成功,颁发token
登录成功,颁发token
将服务端发来的token存储
将服务端发来的token存储
携带token,发送请求
携带token,发送请求
Viewer does not support full SVG 1.1

这种好处是token只需要存储到客户端,服务端只需要对发来的请求中验证token的有效性。

本次便使用基于token的方式,结合spring security进行一次简单的身份认证与授权。

# 相关版本信息

名称 版本
IDEA商业版 2020.1
JDK JDK1.8
Maven 3.5.4
Windows 家庭版1903

# 项目结构

.
├── .idea
├── src
│   └── main
|       ├── java
|       |   └── com
|       |       └── example
|       |           ├── controller
|       |           |   └── HelloResource.java
|       |           ├── filters
|       |           |   └── JwtRequestFilter.java
|       |           ├── model
|       |           |   ├── AuthenticationRequest.java
|       |           |   └── AuthenticationResponse.java
|       |           ├── security
|       |           |   ├── MyUserDetailsService.java
|       |           |   └── SecurityConfigurer.java
|       |           ├── utils
|       |           |   └── JwtUtil.java     
|       |           └── Application.java
│       └── resources
│           └── application.properties
├── test
├── target
├── pom.xml
└── security-jwt.iml 

# 在pom.xml添加相关jar包

<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.0</version>
</dependency>

# 创建Application.java

这个其实就是Spring Boot的入口文件,名称不一样也没事,内容也没有改动。

package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

# 创建SecurityConfigurer.java

这个类是Spring Security的配置类,Spring Boot提倡去掉配置文件,用配置类来代替,道理都差不多,我还是熟悉xml一些。

package com.example.security;
import com.example.filters.JwtRequestFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    JwtRequestFilter jwtRequestFilter;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(bCryptpasswordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/authenticate")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Bean
    public BCryptPasswordEncoder bCryptpasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

# 创建MyUserDetailsService.java

这个类是通过传来用户的username,返回一个用户对象,这里为了简便没有从数据库进行查询,以后改成从数据库访问用户信息,直接在这里查询并返回一个用户就行了。

这里密码采用了BCR加密。

package com.example.security;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@Service
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new User("foo",new BCryptPasswordEncoder().encode("foo"),new ArrayList<>());
    }
}

# 创建JwtUtil.java

这个是Jwt的配置类,可以配置tokenSECRET_KEY,到期时间等等,更重要的作用是可以生成一个token

package com.example.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
    private String SECRET_KEY = "secret";
    public String extractUsername(String token){
        return extractClaim(token, Claims::getSubject);
    }
    public Date extractExpiration(String token){
        return extractClaim(token,Claims::getExpiration);
    }
    public <T> T extractClaim(String token, Function<Claims,T> claimsResolver){
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
    public Claims extractAllClaims(String token){
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
    public Boolean isTokenExpired(String token){
        return extractExpiration(token).before(new Date());
    }
    public String generateToken(UserDetails userDetails){
        Map<String,Object> claims = new HashMap<>();
        return createToken(claims,userDetails.getUsername());
    }
    private String createToken(Map<String,Object> claims,String subject){
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis()+100*60*60*10))
                .signWith(SignatureAlgorithm.HS256,SECRET_KEY)
                .compact();
    }
    public Boolean validateToken(String token,UserDetails userDetails){
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername())) && (!isTokenExpired(token));
    }
}

# 创建AuthenticationRequest.java

这个类的作用是将登录请求信息封装成一个对象,登录的对象是用户,日后添加访问数据库便有了用户类,就不再需要这个类了。

package com.example.model;
public class AuthenticationRequest {
    private String username;
    private String password;
    public AuthenticationRequest() {
    }
    public AuthenticationRequest(String username, String password) {
        this.username = username;
        this.password = password;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

# 创建AuthenticationResponse.java

这个类的作用同样是将信息封装成类,只不过这次是发送出去,即响应请求,我觉得封装成一个Map要更好一点,省的多创建一个类。

package com.example.model;
public class AuthenticationResponse {
    private final String jwt;
    public AuthenticationResponse(String jwt) {
        this.jwt = jwt;
    }
    public String getJwt() {
        return jwt;
    }
}

# 创建JwtRequestFilter.java

这个类检查token是否有效,继承了OncePerRequestFilter类,简单翻译为一次请求的过滤链,也就是说,每次请求都需要这条过滤链的验证,通过了就可以放行,不通过就干掉。

package com.example.filters;
import com.example.security.MyUserDetailsService;
import com.example.utils.JwtUtil;
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.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;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    MyUserDetailsService userDetailsService;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");
        String username = null;
        String jwt = null;
        if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }
        if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if(jwtUtil.validateToken(jwt,userDetails)) {
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails,null,userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                        .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request,response);
    }
}

仅仅写此类还是没有作用的,还需要添加到Spring Security的过滤链中,具体是这个方法http.addFilterBefore(),这个应该改是在Security之前,也有添加到之后的方法,具体有四个相应的方法。我知道自己写的过滤链需要添加上,为什么后面要加一个class类,不太明白。

http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

# 创建HelloResource.java

这个类就没啥说的了,接收和响应用户的请求。

package com.example.controller;
import com.example.security.MyUserDetailsService;
import com.example.model.AuthenticationRequest;
import com.example.model.AuthenticationResponse;
import com.example.utils.JwtUtil;
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.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
public class HelloResource {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private JwtUtil jwtTokenUtil;
    @RequestMapping("/hello")
    public String hello() {
        return "Hello World";
    }
    @PostMapping("/authenticate")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
        try{
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            authenticationRequest.getUsername(),
                            authenticationRequest.getPassword()
                    )
            );
        } catch (BadCredentialsException e) {
            throw new Exception("Incorrect username or password",e);
        }
        final UserDetails userDetails = myUserDetailsService
                .loadUserByUsername(authenticationRequest.getUsername());
        final String jwt = jwtTokenUtil.generateToken(userDetails);
        return ResponseEntity.ok(new AuthenticationResponse(jwt));
    }
}

# 测试

至此,已经完成了完成了所有的代码,现在进行测试。推荐使用postman

首先访问/authenticate接口,获取到token,然后携带token访问/hello获取到正确的信息。

注意一下,下面的接口路径需要自己添加前缀http://localhost:8080/,根据自己实际情况修改。

# 登录验证接口

  • 请求路径:authenticate
  • 请求方法:post
  • 请求参数
参数名 参数说明 备注
username 用户名 不能为空
password 密码 不能为空
  • 请求头
参数名 备注
content-type application/json header
  • 响应参数
参数名 参数说明 备注
jwt 令牌 基于 jwt 的令牌
  • 响应数据
{
    "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmb28iLCJleHAiOjE1OTU2MDQ5NTIsImlhdCI6MTU5NTYwMTM1Mn0.Lk1v9FUCNLlnYmZzEFIcMB9nYPcQgYCoxv2Mg_jklpo"
}

# 携带token获取信息

  • 请求路径:hello

  • 请求方法:get

  • 请求参数

  • 请求头

参数名 备注
content-type application/json header
Authorization Bearer + 获取到的token header

Bearertoken之间有空格。获取到的token也就是上面获取到的jwt的值。

  • 响应参数

  • 响应数据

Hello World

# 项目源码

项目源码地址:https://github.com/srcrs/security-jwt ,或者点我 (opens new window)

# 参考链接

security-jwt整合视频教程 (opens new window)

token与cookie的比较 (opens new window)