SpringSecurity构建基于JWT的登录认证实现
作者:locotor在掘金 发布时间:2023-06-14 10:49:02
目录
目标功能点
准备工作
引入 Maven 依赖
配置 DAO 数据层
创建 JWT 工具类
登录
LoginFilter
LoginSuccessHandler
LoginFailureHandler
验证
JwtAuthenticationFilter
AuthenticationEntryPoint
集中配置
最近项目的登录验证部分,采用了 JWT 验证的方式。并且既然采用了 Spring Boot 框架,验证和权限管理这部分,就自然用了 Spring Security。这里记录一下具体实现。
在项目采用 JWT 方案前,有必要先了解它的特性和适用场景,毕竟软件工程里,没有银弹。只有合适的场景,没有万精油的方案。
一言以蔽之,JWT 可以携带非敏感信息,并具有不可篡改性。可以通过验证是否被篡改,以及读取信息内容,完成网络认证的三个问题:“你是谁”、“你有哪些权限”、“是不是冒充的”。
为了安全,使用它需要采用 Https 协议,并且一定要小心防止用于加密的密钥泄露。
采用 JWT 的认证方式下,服务端并不存储用户状态信息,有效期内无法废弃,有效期到期后,需要重新创建一个新的来替换。
所以它并不适合做长期状态保持,不适合需要用户踢下线的场景,不适合需要频繁修改用户信息的场景。因为要解决这些问题,总是需要额外查询数据库或者缓存,或者反复加密解密,强扭的瓜不甜,不如直接使用 Session。不过作为服务间的短时效切换,还是非常合适的,就比如 OAuth 之类的。
目标功能点
通过填写用户名和密码登录。
验证成功后, 服务端生成 JWT 认证 token, 并返回给客户端。
验证失败后返回错误信息。
客户端在每次请求中携带 JWT 来访问权限内的接口。
每次请求验证 token 有效性和权限,在无有效 token 时抛出 401 未授权错误。
当发现请求带着的 token 有效期快到了的时候,返回特定状态码,重新请求一个新 token。
准备工作
引入 Maven 依赖
针对这个登录验证的实现,需要引入 Spring Security、jackson、java-jwt 三个包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.1</version>
</dependency>
配置 DAO 数据层
要验证用户前,自然是先要创建用户实体对象,以及获取用户的服务类。不同的是,这两个类需要实现 Spring Security 的接口,以便将它们集成到验证框架中。
User
用户实体类需要实现 ”UserDetails“ 接口,这个接口要求实现 getUsername、getPassword、getAuthorities 三个方法,用以获取用户名、密码和权限。以及 isAccountNonExpired```isAccountNonLocked、isCredentialsNonExpired、isEnabled 这四个判断是否是有效用户的方法,因为和验证无关,所以先都返回 true。这里图方便,用了 lombok。
@Data
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
...
}
UserService
用户服务类需要实现 “UserDetailsService” 接口,这个接口非常简单,只需要实现 loadUserByUsername(String username) 这么一个方法。这里使用了 MyBatis 来连接数据库获取用户信息。
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
@Transactional
public User loadUserByUsername(String username) {
return userMapper.getByUsername(username);
}
...
}
创建 JWT 工具类
这个工具类主要负责 token 的生成,验证,从中取值。
@Component
public class JwtTokenProvider {
private static final long JWT_EXPIRATION = 5 * 60 * 1000L; // 五分钟过期
public static final String TOKEN_PREFIX = "Bearer "; // token 的开头字符串
private String jwtSecret = "XXX 密钥,打死也不能告诉别人";
...
}
生成 JWT:从以通过验证的认证对象中,获取用户信息,然后用指定加密方式,以及过期时间生成 token。这里简单的只加了用户名这一个信息到 token 中:
public String generateToken(Authentication authentication) {
User userPrincipal = (User) authentication.getPrincipal(); // 获取用户对象
Date expireDate = new Date(System.currentTimeMillis() + JWT_EXPIRATION); // 设置过期时间
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // 指定加密方式
return JWT.create().withExpiresAt(expireDate).withClaim("username", userPrincipal.getUsername())
.sign(algorithm); // 签发 JWT
} catch (JWTCreationException jwtCreationException) {
return null;
}
}
验证 JWT:指定和签发相同的加密方式,验证这个 token 是否是本服务器签发,是否篡改,或者已过期。
public boolean validateToken(String authToken) {
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // 和签发保持一致
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(authToken);
return true;
} catch (JWTVerificationException jwtVerificationException) {
return false;
}
}
获取荷载信息:从 token 的荷载部分里解析用户名信息,这部分是 md5 编码的,属于公开信息。
public String getUsernameFromJWT(String authToken) {
try {
DecodedJWT jwt = JWT.decode(authToken);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException jwtDecodeException) {
return null;
}
}
登录
登录部分需要创建三个文件:负责登录接口处理的 * ,登陆成功或者失败的处理类。
LoginFilter
Spring Security 默认自带表单登录,负责处理这个登录验证过程的过滤器叫“UsernamePasswordAuthenticationFilter”,不过它只支持表单传值,这里用自定义的类继承它,使其能够支持 JSON 传值,负责登录验证接口。
这个 * 只需要负责从请求中取值即可,验证工作 Spring Security 会帮我们处理好。
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("登录接口方法不支持: " + request.getMethod());
}
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
Map<String, String> loginData = new HashMap<>();
try {
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
}
String username = loginData.get(getUsernameParameter());
String password = loginData.get(getPasswordParameter());
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,
password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
return super.attemptAuthentication(request, response);
}
}
}
LoginSuccessHandler
负责在登录成功后,生成 JWT 给前端。
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
ResponseData responseData = new ResponseData();
String token = jwtTokenProvider.generateToken(authentication);
responseData.setData(JwtTokenProvider.TOKEN_PREFIX + token);
response.setContentType("application/json;charset=utf-8");
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getWriter(), responseData);
}
}
LoginFailureHandler
验证失败后,返回错误信息。
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
ResponseData respBean = setResponseData(exception);
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getWriter(), respBean);
}
private ResponseData setResponseData(AuthenticationException exception) {
if (exception instanceof LockedException) {
return ResponseData.build("用户已被锁定");
} else if (exception instanceof CredentialsExpiredException) {
return ResponseData.build("密码已过期");
} else if (exception instanceof AccountExpiredException) {
return ResponseData.build("用户名已过期");
} else if (exception instanceof DisabledException) {
return ResponseData.build("账户不可用");
} else if (exception instanceof BadCredentialsException) {
return ResponseData.build("验证失败");
}
return ResponseData.build("登录失败,请联系管理员");
}
}
验证
在成功登陆后,前端在每次发起请求时携带签发的 JWT,让服务端能识别这是已登录的用户。
同时,如果未携带 JWT,或携带的 token 过期,或者非法,用单独的处理类返回错误信息。
JwtAuthenticationFilter
负责在每次请求中,解析请求头中的 JWT,从中取得用户信息,生成验证对象传递给下一个过滤器。
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
UsernamePasswordAuthenticationToken authentication = verifyToken(jwt);
if (authentication != null) {
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
}
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
logger.error("无法给 Security 上下文设置用户验证对象", e);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken == null || !bearerToken.startsWith(JwtTokenProvider.TOKEN_PREFIX)) {
logger.info("请求头不含 JWT token,调用下个过滤器");
return null;
}
return bearerToken.split(" ")[1].trim();
}
// 验证token,并生成认证后的token
private UsernamePasswordAuthenticationToken verifyToken(String token) {
if (token == null) {
return null;
}
// 认证失败,返回null
if (!jwtProvider.validateToken(token)) {
return null;
}
// 提取用户名
String username = jwtProvider.getUsernameFromJWT(token);
UserDetails userDetails = new User(username);
// 构建认证过的token
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
AuthenticationEntryPoint
这个类就比较简单,只是在验证不通过后,返回 401 响应,并记录错误信息。
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.error("验证为通过. 提示信息 - {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
集中配置
Spring Security 的功能是通过一系列的过滤器链实现的,而配置整个 Spring Security,只需要统一在一个类中配置即可。
现在咱们就创建这个类,继承自 “WebSecurityConfigurerAdapter”,把上面准备好的各种文件,一一配置进去。
首先是通过注解,设置打开全局的 Spring Security 功能,并通过依赖注入,引入刚刚创建的类。
@Configuration
@EnableWebSecurity
public class KanpmSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public LoginFilter loginFilter(LoginSuccessHandler loginSuccessHandler, LoginFailureHandler loginFailureHandler)
throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
loginFilter.setAuthenticationFailureHandler(loginFailureHandler);
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setFilterProcessesUrl("/auth/login");
return loginFilter;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
...
}
接着,再把用户获取服务类和加密方式,配置到 Spring Security 中去,让它知道如何去验证登录。
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
最后,将JWT过滤器放入过滤器链中,用自定义的登录过滤器替代默认的 “UsernamePasswordAuthenticationFilter”,完成功能。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().anyRequest().authenticated().and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAt(loginFilter(new LoginSuccessHandler(), new LoginFailureHandler()),
UsernamePasswordAuthenticationFilter.class);
}
来源:https://juejin.cn/post/6925991649860386824
![](https://www.aspxhome.com/images/zang.png)
![](https://www.aspxhome.com/images/jiucuo.png)
猜你喜欢
- 由于一些不可控因素的影响,比如系统内存,计算机状态等,每一次在while循环中执行的次数会有一定差异大概几百次。这就导致了结果的差异。注意这
- 本文以实例形式展示了C#与js实现去除textbox文本框里面重复记录的方法!具体方法如下:现有如下问题:页面有一个textbox文本框(是
- 环境准备创建 Maven 项目创建服务器远程连接Tools------Delployment-----Browse Remote Host设
- 1.背景Java语言相比于C和C++,一个最大的特点就是不需要程序员自己手动去申请和释放内存,这一切交由JVM来完成。在Java中,运行时的
- 那么什么是性能测试,它与功能测试有什么样的区别?性能测试是通过自动化的测试工具模拟多种正常、峰值以及异常负载条件来对系统的各项性能指标进行测
- 本文实例讲述了Android编程实现将ButtonBar放在屏幕底部的方法。分享给大家供大家参考,具体如下:前面一篇《Android编程实现
- foreach遍历LIST读到数据为null当我们在使用mybatis的时候,就避免不了批量更新,或者批量查询使用数组或者list,就避免不
- 项目里使用了Feign进行远程调用,有时为了问题排查,需要开启请求和响应日志下面简介一下如何开启Feign日志:注:本文基于spring-b
- 异常日志[com.alibaba.dubbo.rpc.filter.TimeoutFilter] - [DUBBO] invok
- RTF文档即富文本格式(Rich Text Format)的文档。我们在处理文件时,遇到需要对文档格式进行转换时,可以将RTF转为其他格式,
- 1.Java内存模型JAVA定义了一套在多线程读写共享数据时时,对数据的可见性、有序性和原子性的规则和保障。屏蔽掉不同操作系统间的微小差异。
- MyBatis 通过包含的jdbcType类型BIT FLOAT CHAR &nbs
- 茫茫人海千千万万,感谢这一秒你看到这里。希望我的能对你的有所帮助!共勉!愿你在未来的日子,保持热爱,奔赴山海!Java基础知识(多态)多态因
- 一、表创建一、表创建//创建一个空表DataTable dt = new DataTable();//创建一个名为"Table_N
- 一直到大四才开始写自己的第一篇博客,说来实在有点羞愧。今天写了关于排序的算法题,有插入排序,冒泡排序,选择排序,以下贴上用JAVA实现的代码
- 前言 短时间提升自己最快的手段就是背面试题,最近总结了Java常用的面试题,分享给大家,希望大家都能圆梦大厂,加油,我命由我不由天
- SpringBatch介绍:SpringBatch 是一个大数据量的并行处理框架。通常用于数据的离线迁移,和数据处理,⽀持事务、并发、流程、
- 定义注解也叫原数据,它是JDK1.5及之后版本引入的一个特性,它可以声明在类、方法、变量等前面,用来对这些元素进行说明。作用生成文档:通过代
- 方法1 :利用Struts 2的支持的可配置结果,可以达到过滤器的效果。Action的处理结果配置支持正则表达式。但是如果返回的对象是一个数
- 完整代码已上传到GitHub。Web端体验地址:http://47.116.72.33/(只剩一个月有效期)apk下载地址:https://