基于SpringBoot+SpringSecurity+JWT的登录注册(一)

2,973 阅读13分钟

环境:Java8+SpringBoot 2.3.8+Spring Security 5.3.6 + java-jwt 3.12.0

主要是利用过滤器拦截(Filter)

实现了连接数据库,注册登录用户,使用Spring Security结合JWT实现安全认证。

前后端分离,使用json提交数据

Spring Security

Spring Security是一个功能强大、高度可定制的,并且专注于向Java应用提供**身份验证(authentication )授权(authorization)**的框架。

authentication与authorization

authentication主要是确定你是谁,而authorization是可以赋予你访问资源,做某些事的权利。

JWT(JSON Web Token)

JWT主要是用来确定用户的身份信息。在用户第一次携带用户名和密码访问服务器时,服务器签发一个token给用户。接下来用户携带着这个token访问服务器,就不再需要用户密码再次登录。

具体可参考该博客 什么是 JWT -- JSON WEB TOKEN

表数据结构

本次用例采用MongoDB,这里的数据库实现的并不是很重要,大家可以随意:)

tb_user

tb_role

tb_resource

tb_role_res

SpringBoot

新建一个SpringBoot项目

本项目使用了lombok插件

@Data主要用于生成gettersetter

@AllArgsConstructor生成全参构造函数

@NoArgsConstructor生成无参构造函数

@Document(value = "tb_user")关联MongoDB的表

@Id声明表的主键

  • 如果你不使用lombok插件,按照往常写法即可
  • 与表的关联等操作,按照对应数据库的方法操作

有关的Json操作使用了fastjson

maven

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.73</version>
</dependency>

pom.xml

完整的pom.xml文件

<?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
          https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <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>
        <!-- jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.12.0</version>
        </dependency>
        <!-- json -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.73</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

entity

UserDo.java

package com.example.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(value = "tb_user")
public class UserDo {

    @Id
    private String id;          //Id
    private String username;    //用户名
    private String password;    //密码
    private String role_name;   //角色名

    public UserDo(String username, String password, String role_name){
        this.username = username;
        this.password = password;
        this.role_name = role_name;
    }
}

RoleDo.java

package com.example.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(value = "tb_role")
public class RoleDo {

    @Id
    private Integer id;     //Id
    private String name;    //角色名
}

ResourceDo.java

package com.example.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(value = "tb_resource")
public class ResourceDo {

    @Id
    private Integer id;     //Id
    private String name;    //资源名
    private String desc;    //描述
}

RoleResourceDo.java

package com.example.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(value = "tb_role_res")
public class RoleResourceDo {

    @Id
    private String id;          //Id
    private String role_name;   //角色名
    private String res_name;    //资源名

}

dao

UserDao.java

package com.example.demo.dao;

import com.example.demo.entity.UserDo;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface UserDao extends MongoRepository<UserDo,String> {

    /**
     * 根据用户名查询用户
     * @param username  用户名
     * @return  {@code UserDo} 用户对象
     */
    UserDo getUserDoByUsername(String username);
}

service

UserService.java

package com.example.demo.service;

import com.example.demo.entity.UserDo;

import java.util.List;

public interface UserService {

    /**
     * 根据用户名获取用户
     * @param username  用户名
     * @return 用户存在时,返回{@code UserDo}用户对象,
     *          否则返回{@code null}
     */
    UserDo getUserByUsername(String username);

    /**
     * 根据用户名获取用户角色
     * @param username  用户名
     * @return {@code String[]} 角色字符串数组
     */
    String[] getRolesByUser(String username);

    /**
     * 添加一个新用户
     * @param username 用户名
     * @param password 密码
     * @return 创建成功返回{@code UserDo}
     */
    UserDo addUser(String username, String password);

    /**
     * 获取全部用户列表
     * @return {@code List<UserDo>} 用户列表
     */
    List<UserDo> getUserList();

    /**
     * 根据Header中的token获取用户信息
     * @return {@code UserDo} 用户信息
     */
    UserDo getUserByToken();
}

impl

UserServiceImpl.java

package com.example.demo.service.impl;

import com.example.demo.dao.UserDao;
import com.example.demo.entity.UserDo;
import com.example.demo.service.UserService;
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.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    public UserDo getUserByUsername(String username) {
        UserDo userDo = userDao.getUserDoByUsername(username);
        if(userDo==null) return null;
        return userDo;
    }

    @Override
    public String[] getRolesByUser(String username) {
        UserDo userDo = getUserByUsername(username);
        String role_tmp = userDo.getRole_name();
        String[] roles = role_tmp.split(",");
        return roles;
    }

    @Override
    public UserDo addUser(String username, String password) {
        //使用Spring Security提供的BCryptPasswordEncoder加密用户密码存入数据库
        //默认新加入的用户角色为user
        return userDao.save(new UserDo(username, new BCryptPasswordEncoder().encode(password),"user"));
    }

    @Override
    public List<UserDo> getUserList() {
        return userDao.findAll();
    }

    @Override
    public UserDo getUserByToken() {
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) 		     SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        return userDao.getUserDoByUsername(user.getUsername());
    }
}

controller

UserController.java

package com.example.demo.controller;

import com.alibaba.fastjson.JSONObject;
import com.example.demo.common.CodeMsg;
import com.example.demo.common.Result;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 获取全部用户信息列表
     * @return
     */
    @RequestMapping("/admin/getUserList")
    @ResponseBody
    public Result getUserList(){
        return new Result(CodeMsg.SUCCESS,userService.getUserList());
    }

    /**
     * 根据Header中的token获取用户信息
     * @return
     */
    @RequestMapping("/user/info")
    @ResponseBody
    public Result getUserInfo(){
        return new Result(CodeMsg.SUCCESS,userService.getUserByToken());
    }

    /**
     * 根据用户名和密码创建新用户
     * @param jsonObject username,password
     * @return
     */
    @PostMapping("/register")
    @ResponseBody
    public Result register(@RequestBody JSONObject jsonObject){
        String username = jsonObject.getString("username");
        String password = jsonObject.getString("password");
        return new Result(CodeMsg.REGISTER_SUCCESS,userService.addUser(username,password));
    }
}

Spring Security

Spring Security是通过一系列的Filter(过滤器)来实现它的功能的

Filter的顺序与作用

以下是默认的Filter顺序

图片来源

在debug模式下可以看到过滤器执行的顺序

其中3,4是我自定义的过滤器,以下几种方法是用来添加过滤器的

public HttpSecurity addFilterAfter(Filter filter, Class<? extends Filter> afterFilter);	   //在某个过滤器后添加一个过滤器
public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter);  //在某个过滤器前添加一个过滤器
//添加一个过滤器,该过滤器必须继承filters中的过滤器,也就是该过滤器的自定义过滤器
public HttpSecurity addFilter(Filter filter)				
//在某一个过滤器的同一位置添加一个过滤器,但该过滤器不会覆盖原有的过滤器;并且这两个过滤器的执行顺序是不确定的
public HttpSecurity addFilterAt(Filter filter, Class<? extends Filter> atFilter);

filters

  • ChannelProcessingFilter

  • SecurityContextPersistenceFilter

  • LogoutFilter

  • X509AuthenticationFilter

  • AbstractPreAuthenticatedProcessingFilter

  • CasAuthenticationFilter

  • UsernamePasswordAuthenticationFilter

  • OpenIDAuthenticationFilter

  • DefaultLoginPageGeneratingFilter

  • DefaultLogoutPageGeneratingFilter

  • ConcurrentSessionFilter

  • DigestAuthenticationFilter

  • BearerTokenAuthenticationFilter

  • BasicAuthenticationFilter

  • RequestCacheAwareFilter

  • SecurityContextHolderAwareRequestFilter

  • JaasApiIntegrationFilter

  • RememberMeAuthenticationFilter

  • AnonymousAuthenticationFilter

  • SessionManagementFilter

  • ExceptionTranslationFilter

  • FilterSecurityInterceptor

  • SwitchUserFilter

Spring Security Web 5.1.2 源码解析 -- 安全相关Filter清单

SpringSecurity过滤器顺序

认证流程

图片来源

图片来源

以上两张图很清晰地展现了Spring Security的主要认证流程。

在接收到请求后,先经过一系列的过滤器。

当被UsernamePasswordAuthenticationFilter拦截到后,调用其attemptAuthentication(request,response)方法,获取到username和password后,封装成UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);

再让AuthenticationManager调用authenticate(authentication)方法进行验证。

return this.getAuthenticationManager().authenticate(authenticationToken);

AuthenticationManager的默认实现是ProviderManager,其内部维护着一个AuthenticationProvider的列表。这个列表中存放的就是就是各种认证方式。当调用ProviderManagerauthenticate(authentication)方法时,会遍历该列表。

当认证成功时,会返回一个经过身份验证的对象Authentication,并且不会执行后面的认证方法。若全部验证失败,则会抛出AuthenticationException异常。

//部分ProviderManager的authenticate(Authentication authentication)方法源代码
for (AuthenticationProvider provider : getProviders()) {	//遍历该列表
		if (!provider.supports(toTest)) {
			continue;
		}

		if (debug) {
			logger.debug("Authentication attempt using "
					+ provider.getClass().getName());
		}

		try {
			result = provider.authenticate(authentication);		//调用认证方法

			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
		}
		catch (AccountStatusException | InternalAuthenticationServiceException e) {
			prepareException(e, authentication);
			// SEC-546: Avoid polling additional providers if auth failure is due to
			// invalid account status
			throw e;
		} catch (AuthenticationException e) {
			lastException = e;
		}
}

AuthenticationProvider的一个实现AbstractUserDetailsAuthenticationProvider会响应传过来的UsernamePasswordAuthenticationToken身份验证请求。

AbstractUserDetailsAuthenticationProvider调用authenticate(authentication)方法,根据传过来的Authentication获取用户名后,从缓存或者调用retrieveUser(username, authentication)方法。该方法由AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider实现,主要是调用了UserDetailsServiceloadUserByUsername(username)方法加载用户信息。我们一般会重写该方法,从数据库中取出用户信息。

在获取了正确的用户信息UserDetails和根据前面传数据过来后封装的UsernamePasswordAuthenticationToken后,调用DaoAuthenticationProvider实现的additionalAuthenticationChecks(userDetails,authentication)比较两者的密码是否一致完成验证。

//retrieveUser方法源码
protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            //此处可以看出调用了UserDetailsService().loadUserByUsername(username)
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

如图是AuthenticationManagerAuthenticationProvider等几个类的关系

Json提交数据登录

因为Spring Security是默认通过form表单提交数据进行登录验证的。所以要通过URL访问后台并提交Json数据,需要修改Spring Security的配置。

在查阅资料的过程中,我发现在改写登录身份验证时,有的是从UsernamePasswordAuthenticationFilter继承,有的是从AbstractAuthenticationProcessingFilter继承。

UsernamePasswordAuthenticationFilter和AbstractAuthenticationProcessingFilter的区别

默认是使用UsernamePasswordAuthenticationFilter来拦截表单登录请求的,而UsernamePasswordAuthenticationFilter是从AbstractAuthenticationProcessingFilter继承而来的。

UsernamePasswordAuthenticationFilter

是用来处理表单登录的,默认登录URL为/login;需要提供两个参数:用户名和密码,也有默认的参数名,分别为usernamepassword。要修改它的认证方法,主要是通过重写 attemptAuthentication(request,response)方法。

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response);

若是使用表单登录,可以在继承WebSecurityConfigurerAdapter的类里修改

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //配置登录请求相关内容
        http.formLogin()
                .usernameParameter("user")
                .passwordParameter("pswd")
                .loginPage("/toLogin")      //登录页
                .loginProcessingUrl("/login");   //登录表单提交地址
    }
}

若是使用Json传递数据登录,可以参考里的JwtLoginFilter

AbstractAuthenticationProcessingFilter

是基于浏览器HTTP的身份验证请求处理器

继承该类时,主要需要设置三个地方

  1. 设置authenticationManager属性,是用来处理身份认证请求的token
  2. 需要设置RequestMatcher设置拦截登录用的URL
  3. 实现attemptAuthentication()方法
AuthenticationSuccessHandler

当验证成功时,会调用该Handler,默认实现是SavedRequestAwareAuthenticationSuccessHandler。它将用户重定向到ExceptionTranslationFilter中设置的DefaultSavedRequest,否则会重定向到Web应用程序的根目录。

也可以在验证成功后,重写successfulAuthentication()方法,调用顺序是先执行successfulAuthentication(),再执行Handler。要注意的是,如果重写successfulAuthentication()时,没有调用chain.doFilter(request, response),则不会再调用Handler

AuthenticationFailureHandler

默认实现是SimpleUrlAuthenticationFailureHandler。它向客户端发送401错误代码,也可以配置失败的URL

package com.example.demo.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.util.ServletUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 自定义的身份验证过滤器
 */
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public CustomAuthenticationFilter() {
        //拦截 "/login" 的请求
        super(new AntPathRequestMatcher("/login","POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        // 因request.getParameter()不能获取到application/json中的数据
        // 需要把用户名密码的读取逻辑修改为到流中读取request.getInputStream()
        String body = ServletUtil.getBody(request);
        JSONObject jsonObject = JSON.parseObject(body);
        String username = jsonObject.getString("username");
        String password = jsonObject.getString("password");


        if(username == null){
            username = "";
        }

        if(password == null){
            password = "";
        }

        username = username.trim();

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);

        return this.getAuthenticationManager().authenticate(authenticationToken);
    }
}

从数据库加载用户信息

上面提到过,DaoAuthenticationProviderretrieveUser(username, authentication)方法中调用了UserDetailsServiceloadUserByUsername(username)方法。所以我们继承UserDetailsService接口,实现该方法。

package com.example.demo.service.impl;

import com.example.demo.entity.UserDo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Arrays;

/**
 * 从数据库中加载用户信息
 */
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    /**
     *
     * @param username  用户名
     * @return {@code UserDetails}
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDo userDo = userService.getUserByUsername(username);
        if(userDo==null){
            throw  new UsernameNotFoundException("用户不存在");
        }
        // 查询成功后,用户存在,需要匹配用户密码是否正确
        // 匹配密码是由 Spring Security 内部逻辑自动完成
        //用户登录成功后,查询用户的权限集合。
        String[] roles = userService.getRolesByUser(username);

        String[] authorities = new String[roles.length];
        for(int i = 0; i < roles.length; i++){
            authorities[i] = "ROLE_" + roles[i];
        }
        
        System.out.println("用户" + userDo.getUsername() + "的权限集合是:" + Arrays.toString(authorities));

        org.springframework.security.core.userdetails.User result =
                new org.springframework.security.core.userdetails.User(username,userDo.getPassword(), AuthorityUtils.createAuthorityList(authorities));

        return result;
    }
}

JWT

JWT官网里面有各种语言的JWT实现,这里选择的是auth0的

maven

<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.12.0</version>
</dependency>

JWT工具类

package com.example.demo.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Date;
import java.util.function.Function;

/**
 * jwt 工具类
 */
public class JwtTokenUtil {

    public static final int JWT_TOKEN_VALIDITY = 30;    //Token有效期,单位:分钟

    public static final String SECRET = "Secret";       //私钥

    /**
     * 根据token获取用户名
     * @param token
     * @return {@code String} 用户名
     */
    public static String getAudienceByToken(String token){
        String audience = null;
        try {
            audience = JWT.decode(token).getAudience().get(0);
        }catch (JWTDecodeException e){
            e.printStackTrace();
            throw new JWTDecodeException("jwt token解码失败");
        }
        return audience;
    }

    /**
     * 根据token和实体名获取自定义实体
     * @param token
     * @param name  实体名
     * @return {@code Claim}
     */
    public static Claim getClaimByName(String token, String name){
        return JWT.decode(token).getClaim(name);
    }

    /**
     * 根据用户名和私钥生成token
     * @param username 用户名
     * @return {@code String} token
     */
    public static String generateToken(String username){
        String token = null;

        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.MINUTE,JWT_TOKEN_VALIDITY);
        Date expiresDate = nowTime.getTime();
        try {
            token = JWT.create()
                    .withAudience(username)    //签发对象
                    .withIssuedAt(new Date())   //发行时间
                    .withExpiresAt(expiresDate)//有效时间
                    .sign(Algorithm.HMAC256(username+SECRET));  //加密算法

        }catch (JWTCreationException exception){
            exception.printStackTrace();
        }
        return token;
    }

    /**
     * 根据token和用户名验证token是否正确
     * @param token
     * @param username 用户名
     * @return {@code true} token正确
     *         {@code false} token错误
     */
    public static Boolean validateToken(String token, String username){
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(username+SECRET))
                    .withAudience(username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        }catch (TokenExpiredException e){
            e.printStackTrace();
            return false;
        }catch (JWTVerificationException e){
            e.printStackTrace();
            return false;
        }
    }
}

添加JWT Token

在通过身份验证后,我们为利用用户的用户名和私钥生成一个Jwt token返回给前台。

新建一个JsonLoginSuccessHandler类来处理该逻辑

package com.example.demo.handler;

import com.example.demo.util.JwtTokenUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 身份验证后,在header中添加jwt token
 */
public class JsonLoginSuccessHandler implements AuthenticationSuccessHandler {


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        //添加token
        String token = JwtTokenUtil.generateToken(((UserDetails)authentication.getPrincipal()).getUsername());
        response.addHeader("Authorization",token);
    }
}

配置Filter

配置一个登录失败的处理器

package com.example.demo.handler;

import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登录失败处理器
 * 回复401
 */
public class HttpStatusLoginFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
    }
}

配置Filter

package com.example.demo.config;

import com.example.demo.filter.CustomAuthenticationFilter;
import com.example.demo.handler.HttpStatusLoginFailureHandler;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;

/**
 * 配置CustomAuthenticationFilter
 */
public class JsonLoginConfig<T extends JsonLoginConfig<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B> {

    private CustomAuthenticationFilter authFilter;

    public JsonLoginConfig(){
        this.authFilter = new CustomAuthenticationFilter();
    }

    @Override
    public void configure(B builder) throws Exception {
        //设置Filter使用的AuthenticationManager,这里取公共的
        authFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
        //设置失败的Handler
        authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());
        //不将认证后的context放入session
        authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());

        CustomAuthenticationFilter authenticationFilter = postProcess(authFilter);
        //指定filter的位置
        builder.addFilterAfter(authenticationFilter, LogoutFilter.class);
    }

    //设置成功的Handler,这个handler定义成Bean,所以从外面set进来
    public JsonLoginConfig<T,B> loginSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler){
        authFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        return this;
    }
}

验证Jwt Token

package com.example.demo.filter;

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.demo.service.impl.JwtUserDetailsServiceImpl;
import com.example.demo.util.JwtTokenUtil;
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;

/**
 * 每个需要验证的请求都验证token是否正确
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUserDetailsServiceImpl jwtUserDetailsServiceImpl;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("JwtAuthorizationFilter执行...");
        System.out.println("验证Token...");
        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(authentication == null){
            System.out.println("authentication null");
            filterChain.doFilter(request,response);
            return;
        }

        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request,response);
    }

    /**
     * 根据request中的token,验证后获取UsernamePasswordAuthenticationToken
     * @param request
     * @return
     */
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){

        String username = null;
        String jwtToken = null;

        jwtToken = request.getHeader("Authorization");

        if(jwtToken != null){
            try {
                username = JwtTokenUtil.getAudienceByToken(jwtToken);
            }catch (IllegalArgumentException e){
                e.printStackTrace();
                System.out.println("不能获取token或token不正确");
            }catch (TokenExpiredException e){
                e.printStackTrace();
                System.out.println("token过期");
            }
        }

        //获取token后验证
        if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
            System.out.println("username:"+username);

            UserDetails userDetails = jwtUserDetailsServiceImpl.loadUserByUsername(username);
            if(JwtTokenUtil.validateToken(jwtToken,username)){  //验证token
                //创建UsernamePasswordAuthenticationToken
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken= new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                return usernamePasswordAuthenticationToken;
            }
        }

        return null;
    }
}

package com.example.demo.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 拒绝每个未经身份验证(token)的请求并发送错误代码401
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Unauthorized");
        System.out.println("JWT Unauthorized...");
    }
}

Security配置

package com.example.demo.config;

import com.example.demo.filter.JwtAuthenticationFilter;
import com.example.demo.handler.JsonLoginSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);

        //禁用form登录
        http.formLogin().disable();


        // 配置权限
        http.authorizeRequests()
                .antMatchers("/login","/register").permitAll()
                // 基于角色的权限管理
                .antMatchers("/admin/**","/user/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated();  //任意的请求,都必须认证后才能访问

        // 添加过滤器
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        http.apply(new JsonLoginConfig<>()).loginSuccessHandler(jsonLoginSuccessHandler());

        http.exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint);

        //使用无状态session,session不会储存用户状态
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);


        //关闭CSRF安全协议
        //关闭是为了保证完整流程的可用
        http.csrf().disable();
    }

    // 注入密码编码器对象
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    protected JsonLoginSuccessHandler jsonLoginSuccessHandler(){
        return new JsonLoginSuccessHandler();
    }
}

通用类

package com.example.demo.common;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
public enum CodeMsg {

    REGISTER_SUCCESS(2000,"注册成功"),
    REGISTER_FAILURE(2001,"注册失败"),

    LOGIN_SUCCESS(2002,"登录成功"),
    LOGIN_FAILURE(2003,"登录失败"),

    SUCCESS(2004,"获取数据成功"),
    FAILURE(2005,"获取数据失败")


    ;

    private int code;
    private String message;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

}
package com.example.demo.common;

import lombok.Data;

@Data
public class Result {

    private int code;
    private String message;
    private Object entity;

    public Result(){}
    public Result(CodeMsg codeMsg, Object entity){
        this.code=codeMsg.getCode();
        this.message=codeMsg.getMessage();
        this.entity=entity;
    }

}
package com.example.demo.util;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

public class ServletUtil {

    /**
     * 获取请求中的body
     * @param request
     * @return
     */
    public static String getBody(HttpServletRequest request){
        StringBuilder stringBuilder = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try{
            inputStream = request.getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
            String line = "";
            while ((line = reader.readLine())!=null){
                stringBuilder.append(line);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            if(inputStream != null){
                try {
                    inputStream.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
            if(reader != null){
                try{
                    reader.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
        return stringBuilder.toString();
    }
}

测试

使用Postman进行测试

使用管理员的账号密码进行登录,可以看到返回的header中带有Authorization

下一次请求带上该token,成功获取数据

相同的操作,更换成普通用户登录。

获取token后访问只有管理员能访问的接口,可以看到返回了403没有权限

如果不带token访问,会返回401没有授权

小结

本文主要描述了使用如何使用Spring Security和JWT结合Springboot进行登录验证。实现这个功能,还有多种配置方式。可以使用Form表单登录,前后端结合在一起。还有更多个性化的配置,如不使用UsernamePasswordAuthenticationToken,新建一个Token类;还有使用多种验证方式,邮箱登录等。

参考

Spring Security 案例实现和执行流程剖析

Spring Security做JWT认证和授权

自定义SpringSecurity认证方式

SpringSecurity 核心组件介绍 + 认证流程 +内置拦截器顺序

Spring-Security权限框架

尚硅谷SpringSecurity框架教程