当前位置: 首页 > news >正文

SpringSecurity(07)——JWT整合

依赖配置

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.9.0</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>
<dependency><groupId>com.github.axet</groupId><artifactId>kaptcha</artifactId><version>0.0.9</version>
</dependency>
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.15</version>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId>
</dependency>
<dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

自定义全局返回结果

@Data
public class Result implements Serializable {private int code;private String msg;private Object data;public static Result succ(Object data) {return succ(200, "操作成功", data);}public static Result fail(String msg) {return fail(400, msg, null);}public static Result succ (int code, String msg, Object data) {Result result = new Result();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}public static Result fail (int code, String msg, Object data) {Result result = new Result();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}
}

JWT配置类

jwt:header: Authorizationexpire: 604800  #7天,s为单位secret: 123456
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtUtils {private long expire;private String secret;private String header;/*** 生成JWT* @param username* @return*/public String generateToken(String username){Date nowDate = new Date();Date expireDate = new Date(nowDate.getTime() + 1000 * expire);return Jwts.builder().setHeaderParam("typ","JWT").setSubject(username).setIssuedAt(nowDate).setExpiration(expireDate)  //7天过期.signWith(SignatureAlgorithm.HS512,secret).compact();}/*** 解析JWT* @param jwt* @return*/public Claims getClaimsByToken(String jwt){try {return Jwts.parser().setSigningKey(secret).parseClaimsJws(jwt).getBody();}catch (Exception e){return null;}}/*** 判断JWT是否过期* @param claims* @return*/public boolean isTokenExpired(Claims claims){return claims.getExpiration().before(new Date());}
}

自定义登录处理器

/*** 登录成功控制器*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Autowiredprivate JwtUtils jwtUtils;@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();//生成JWT,并放置到请求头中String jwt = jwtUtils.generateToken(authentication.getName());httpServletResponse.setHeader(jwtUtils.getHeader(), jwt);Result result = Result.succ("SuccessLogin");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
/*** 登录失败控制器*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();String errorMessage = "用户名或密码错误";Result result;if (e instanceof CaptchaException) {errorMessage = "验证码错误";result = Result.fail(errorMessage);} else {result = Result.fail(errorMessage);}outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}

自定义登出处理器

/*** 登出处理器*/
@Component
public class JWTLogoutSuccessHandler implements LogoutSuccessHandler {@Autowiredprivate JwtUtils jwtUtils;@Overridepublic void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {if (authentication!=null){new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);}httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();httpServletResponse.setHeader(jwtUtils.getHeader(), "");Result result = Result.succ("SuccessLogout");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}

验证码配置

public class CaptchaException extends AuthenticationException {public CaptchaException(String msg){super(msg);}
}
/*** 验证码配置*/
@Configuration
public class KaptchaConfig {@Beanpublic DefaultKaptcha producer(){Properties properties = new Properties();properties.put("kaptcha.border", "no");properties.put("kaptcha.textproducer.font.color", "black");properties.put("kaptcha.textproducer.char.space", "4");properties.put("kaptcha.image.height", "40");properties.put("kaptcha.image.width", "120");properties.put("kaptcha.textproducer.font.size", "30");Config config = new Config(properties);DefaultKaptcha defaultKaptcha = new DefaultKaptcha();defaultKaptcha.setConfig(config);return defaultKaptcha;}
}
@RestController
public class CaptchaController {@Autowiredprivate Producer producer;@Autowiredprivate RedisUtil redisUtil;@GetMapping("/captcha")public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {String code = producer.createText();BufferedImage image = producer.createImage(code);redisUtil.set("captcha", code, 120);// 将验证码图片返回,禁止验证码图片缓存response.setHeader("Cache-Control", "no-store");response.setHeader("Pragma", "no-cache");response.setDateHeader("Expires", 0);response.setContentType("image/jpeg");ImageIO.write(image, "jpg", response.getOutputStream());}
}

自定义过滤器

OncePerRequestFilter:在每次请求时只执行一次过滤,保证一次请求只通过一次filter,而不需要重复执行

/*** 验证码过滤器*/
@Component
public class CaptchaFilter extends OncePerRequestFilter {@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate LoginFailureHandler loginFailureHandler;@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {String url = httpServletRequest.getRequestURI();if ("/login/form".equals(url) && httpServletRequest.getMethod().equals("POST")) {//校验验证码try {validate(httpServletRequest);} catch (CaptchaException e) {//交给认证失败处理器loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);return;}}filterChain.doFilter(httpServletRequest, httpServletResponse);}private void validate(HttpServletRequest request) {String code = request.getParameter("code");if (StringUtils.isBlank(code)) {throw new CaptchaException("验证码错误");}String captcha = (String) redisUtil.get("captcha");if (!code.equals(captcha)) {throw new CaptchaException("验证码错误");}//若验证码正确,执行以下语句,一次性使用redisUtil.del("captcha");}
}

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录</title>
</head>
<body>
<h3>表单登录</h3>
<form method="post" th:action="@{/login/form}"><input type="text" name="username" placeholder="用户名"><br><input type="password" name="password" placeholder="密码"><br><input type="text" name="code" placeholder="验证码"><br><img th:onclick="this.src='/captcha?'+Math.random()" th:src="@{/captcha}" alt="验证码"/><br><div th:if="${param.error}"><span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用户名或密码错误</span></div><button type="submit">登录</button>
</form>
</body>
</html>

BasicAuthenticationFilter:OncePerRequestFilter执行完后,由BasicAuthenticationFilter检测和处理http basic认证,取出请求头中的jwt,校验jwt

/*** JWT过滤器*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate UserDetailServiceImpl userDetailService;public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {super(authenticationManager);}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {String jwt = request.getHeader(jwtUtils.getHeader());//这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的//没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口if (StrUtil.isBlankOrUndefined(jwt)) {chain.doFilter(request, response);return;}Claims claim = jwtUtils.getClaimsByToken(jwt);if (claim == null) {throw new JwtException("token异常");}if (jwtUtils.isTokenExpired(claim)) {throw new JwtException("token已过期");}String username = claim.getSubject();User user = UserDetailServiceImpl.userMap.get(username);//构建token,这里密码为null,是因为提供了正确的JWT,实现自动登录UsernamePasswordAuthenticationToken token =new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(user.getId()));SecurityContextHolder.getContext().setAuthentication(token);chain.doFilter(request, response);}
}

自定义权限异常处理器

当BasicAuthenticationFilter认证失败的时候会进入AuthenticationEntryPoint

/*** JWT认证失败处理器*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);ServletOutputStream outputStream = httpServletResponse.getOutputStream();Result result = Result.fail("请先登录");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
/*** 无权限访问的处理*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);ServletOutputStream outputStream = httpServletResponse.getOutputStream();Result result = Result.fail(e.getMessage());outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}

自定义用户登录逻辑

public class AccountUser implements UserDetails {private Long userId;private static final long serialVersionUID = 540L;private static final Log logger = LogFactory.getLog(User.class);private String password;private final String username;private final Collection<? extends GrantedAuthority> authorities;private final boolean accountNonExpired;private final boolean accountNonLocked;private final boolean credentialsNonExpired;private final boolean enabled;public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {this(userId, username, password, true, true, true, true, authorities);}public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");this.userId = userId;this.username = username;this.password = password;this.enabled = enabled;this.accountNonExpired = accountNonExpired;this.credentialsNonExpired = credentialsNonExpired;this.accountNonLocked = accountNonLocked;this.authorities = authorities;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.username;}@Overridepublic boolean isAccountNonExpired() {return this.accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return this.accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return this.credentialsNonExpired;}@Overridepublic boolean isEnabled() {return this.enabled;}
}
@Service
public class UserDetailServiceImpl implements UserDetailsService {public static Map<String, User> userMap = new HashMap<>();static {userMap.put("root", new User("root", "123", AuthorityUtils.createAuthorityList("all")));}@Resourceprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {User user = userMap.get(s);if (user == null) {throw new UsernameNotFoundException("用户名或密码错误");}return new AccountUser(1L, user.getUsername(), passwordEncoder.encode(user.getPassword()), user.getAuthorities());}
}
@Component
public class PasswordEncoder extends BCryptPasswordEncoder {/*** 加密* @param charSequence  明文字符串* @return*/@Overridepublic String encode(CharSequence charSequence) {try {MessageDigest digest = MessageDigest.getInstance("MD5");return toHexString(digest.digest(charSequence.toString().getBytes()));} catch (NoSuchAlgorithmException e) {e.printStackTrace();return "";}}/*** 密码校验* @param charSequence 明文,页面收集密码* @param s 密文 ,数据库中存放密码* @return*/@Overridepublic boolean matches(CharSequence charSequence, String s) {return s.equals(encode(charSequence));}/*** @param tmp 转16进制字节数组* @return 饭回16进制字符串*/private String toHexString(byte [] tmp){StringBuilder builder = new StringBuilder();for (byte b :tmp){String s = Integer.toHexString(b & 0xFF);if (s.length()==1){builder.append("0");}builder.append(s);}return builder.toString();}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@AutowiredLoginFailureHandler loginFailureHandler;@AutowiredLoginSuccessHandler loginSuccessHandler;@AutowiredCaptchaFilter captchaFilter;@AutowiredJwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;@AutowiredJwtAccessDeniedHandler jwtAccessDeniedHandler;@AutowiredUserDetailServiceImpl userDetailService;@AutowiredJWTLogoutSuccessHandler jwtLogoutSuccessHandler;@BeanJwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());return jwtAuthenticationFilter;}private static final String[] URL_WHITELIST = {"/login","/logout","/captcha","/favicon.ico"};@Beanpublic PasswordEncoderImpl passwordEncoder() {return new PasswordEncoderImpl();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable()//登录配置.formLogin().loginPage("/login").loginProcessingUrl("/login/form").successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).and().logout().logoutSuccessHandler(jwtLogoutSuccessHandler)//禁用session.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//配置拦截规则.and().authorizeRequests().antMatchers(URL_WHITELIST).permitAll().anyRequest().authenticated()//异常处理器.and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).accessDeniedHandler(jwtAccessDeniedHandler)//配置自定义的过滤器.and().addFilter(jwtAuthenticationFilter())//验证码过滤器放在UsernamePassword过滤器之前.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());}/*** 定制一些全局性的安全配置,例如:不拦截静态资源的访问*/@Overridepublic void configure(WebSecurity web) throws Exception {// 静态资源的访问不需要拦截,直接放行web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");}
}

相关文章:

  • react和vue的区别
  • 数字身份所有权:Web3时代用户数据的掌控权
  • EXCEL VBA获取幸运数字号码
  • C++---string类
  • Cinder组件作用
  • HarmonyOS鸿蒙学习基础篇 - 项目目录和文件介绍
  • 计算机基础之总线与I/O总线
  • [Linux] Ubuntu install Miniconda
  • Broadcom交换芯片56620架构
  • 嵌入式Linux:如何进行嵌入式Linux开发?
  • 目标检测YOLO实战应用案例100讲-橘子自动采摘机视觉识别
  • R语言——AVOCADO“(异常植被变化检测)算法(1990-2015数据分析)监测森林干扰和再生(含GEE影像下载代码)
  • 甜蜜而简洁——深入了解Pytest插件pytest-sugar
  • Matplotlib Mastery: 从基础到高级的数据可视化指南【第30篇—python:数据可视化】
  • React16源码: React中的updateHostRoot的源码实现
  • angular组件开发
  • Date型的使用
  • golang中接口赋值与方法集
  • Java 9 被无情抛弃,Java 8 直接升级到 Java 10!!
  • JWT究竟是什么呢?
  • Linux后台研发超实用命令总结
  • unity如何实现一个固定宽度的orthagraphic相机
  • Vue 重置组件到初始状态
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 好的网址,关于.net 4.0 ,vs 2010
  • 设计模式 开闭原则
  • 微服务核心架构梳理
  • 在electron中实现跨域请求,无需更改服务器端设置
  • 怎么把视频里的音乐提取出来
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • NLPIR智能语义技术让大数据挖掘更简单
  • 翻译 | The Principles of OOD 面向对象设计原则
  • ​草莓熊python turtle绘图代码(玫瑰花版)附源代码
  • ​什么是bug?bug的源头在哪里?
  • #每日一题合集#牛客JZ23-JZ33
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • (145)光线追踪距离场柔和阴影
  • (2.2w字)前端单元测试之Jest详解篇
  • (c语言)strcpy函数用法
  • (附源码)spring boot儿童教育管理系统 毕业设计 281442
  • (附源码)springboot猪场管理系统 毕业设计 160901
  • (十二)devops持续集成开发——jenkins的全局工具配置之sonar qube环境安装及配置
  • (顺序)容器的好伴侣 --- 容器适配器
  • (转)Google的Objective-C编码规范
  • (转)使用VMware vSphere标准交换机设置网络连接
  • .NET : 在VS2008中计算代码度量值
  • .NET Core 成都线下面基会拉开序幕
  • .NET Reactor简单使用教程
  • .net Stream篇(六)
  • .Net中ListT 泛型转成DataTable、DataSet
  • .NET中统一的存储过程调用方法(收藏)
  • .考试倒计时43天!来提分啦!
  • @ModelAttribute 注解
  • [ C++ ] STL---仿函数与priority_queue
  • [ NOI 2001 ] 食物链