环境: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
主要用于生成getter和setter
@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清单
认证流程
以上两张图很清晰地展现了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
的列表。这个列表中存放的就是就是各种认证方式。当调用ProviderManager
的authenticate(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
实现,主要是调用了UserDetailsService
的loadUserByUsername(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);
}
}
复制代码
如图是AuthenticationManager
和AuthenticationProvider
等几个类的关系
Json提交数据登录
因为Spring Security是默认通过form表单提交数据进行登录验证的。所以要通过URL访问后台并提交Json数据,需要修改Spring Security的配置。
在查阅资料的过程中,我发现在改写登录身份验证时,有的是从UsernamePasswordAuthenticationFilter
继承,有的是从AbstractAuthenticationProcessingFilter
继承。
UsernamePasswordAuthenticationFilter和AbstractAuthenticationProcessingFilter的区别
默认是使用UsernamePasswordAuthenticationFilter
来拦截表单登录请求的,而UsernamePasswordAuthenticationFilter
是从AbstractAuthenticationProcessingFilter
继承而来的。
UsernamePasswordAuthenticationFilter
是用来处理表单登录的,默认登录URL为/login
;需要提供两个参数:用户名和密码,也有默认的参数名,分别为username
和password
。要修改它的认证方法,主要是通过重写 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的身份验证请求处理器
继承该类时,主要需要设置三个地方
- 设置authenticationManager属性,是用来处理身份认证请求的token
- 需要设置RequestMatcher设置拦截登录用的URL
- 实现
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);
}
}
复制代码
从数据库加载用户信息
上面提到过,DaoAuthenticationProvider
的retrieveUser(username, authentication)
方法中调用了UserDetailsService
的loadUserByUsername(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类;还有使用多种验证方式,邮箱登录等。