1 JWT是做什么的?
为了在前后端分离项目中使用 JWT ,我们需要达到 2 个目标:
-
在用户登录认证成功后,需要返回一个含有 JWT token 的 json 串。
-
在用户发起的请求中,如果携带了正确合法的 JWT token ,后台需要放行,运行它对当前 URI 的访问
在spring security项目中添加nimbus坐标即可
<!--nimbus坐标-->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.11.1</version>
</dependency>
2 1、返回 JWT token
Spring Security 中的登录认证功能是由 UsernamePasswordAuthenticationFilter 完成的,默认情况下,在登陆成功后,接下来就是页面跳转,显示你原本想要访问的 URI( 或 /
),现在,我们需要返回 JSON(其中还要包含 JWT token )。
Spring Security 支持通过实现 AuthenticationSuccessHandler 接口,来自定义在登陆成功之后你所要做的事情(之前有讲过这部分内容):
http.formLogin()
.successHandler(new JWTAuthenticationSuccessHandler());
当用户登录成功后,Spring security返回一个jwt的token给客户端,所以要在Spring security登录成功的过滤器中实现jwt token的生成代码。
-
创建spring security项目并添加nimbus坐标
关键坐标如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.woniu</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
</parent>
<dependencies>
<!--springmvc启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--测试启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--springboot整合security坐标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--nimbus坐标-->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.11.1</version>
</dependency>
</dependencies>
</project>
配置认证UserDetailsService
/**
* spring security认证业务类
*/
@Service
public class LoginUserDetailsService implements UserDetailsService {
@Resource
private UserDao userDao;
//为passwordEncoder注入值
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//调用dao到数据库中根据username查找用户信息
Users users = userDao.getByUserName(username);
try {
//将查找到的用户帐号与密码保存到Security的User对象中由Security进行比对
return new User(users.getUsername(), users.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,user:insert,user:delete")); //配置登录用户有哪些角色和权限,此处模拟直接写死
}catch (Exception e){
throw new UsernameNotFoundException("用户"+username+"不存在");
}
}
}
spring security配置类
package com.example.springsecurity3.configuration;
import com.example.springsecurity3.hndler.*;
import com.example.springsecurity3.service.SecurityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class UsersConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityService securityService;
@Bean
public PasswordEncoder getPassword(){
return new BCryptPasswordEncoder();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//给密码加密
auth.userDetailsService(securityService).passwordEncoder(getPassword());
}
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().//告诉security,要是用自己的页面
loginPage("/login.html").//告诉security页面在哪里
loginProcessingUrl("/dologin").//告诉security表但提交地址(
//hasAuthority,hasAnyAuthority,hasRole,hasAnyRole四种方法可以设置访问路径,现在我们用PreAuthorize("hasAuthority('admin')")注解就可以替我们
//访问获取授权,在UserConfig类上加上EnableGlobalMethodSecurity(securedEnabled=true,proPostEnable=true)
//注册登录成功处理器
successHandler(new SecurityHandler()).
//注册登录失败处理器
failureHandler(new LoginFailHandler()).
permitAll();//将登陆的权限放出来
//注册未登录处理器,
http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint())
//注册有用户名和密码,没有授权的处理器
.accessDeniedHandler(new MyAccessDeniedHandler());
//开启访问权限
http.cors();
//注册退出
http.logout().logoutSuccessHandler(new LogoutSuccess());
//配置权限请求
// http.authorizeRequests().anyRequest().authenticated();//关闭所有请求
// http.csrf().disable();//关闭跨站脚本攻击
//配置权限请求
http.authorizeRequests().
anyRequest().
authenticated();//关闭所有请求
http.csrf().disable();//关闭跨站脚本攻击
}
}
配置登录结果异常处理器
登录成功
package com.example.springsecurity3.hndler;
import com.alibaba.fastjson.JSON;
import com.example.springsecurity3.utils.JWTUtils;
import com.example.springsecurity3.utils.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
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;
import java.io.PrintWriter;
/**
* 登录成功提示信息
*/
public class SecurityHandler implements AuthenticationSuccessHandler {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
//把jwt放入redis,并设置有效时间
// try {
// redisTemplate.opsForValue().set("jwt:"+user.getUsername(),
// JWTUtils.createJWT(user.getUsername()));
// redisTemplate.opsForValue().set("jwt:"+user.getUsername(),JWTUtils.createJWT(user.getUsername()));
// } catch (Exception e) {
// e.printStackTrace();
// }
System.out.println(user.getUsername());
response.setContentType("application/json;charset=utf-8");
PrintWriter pw = response.getWriter();
//将json转成字符串
String json = JSON.toJSONString(ResponseResult.SECCUSS);
pw.print(json);
pw.flush();
pw.close();
}
}
登录失败
package com.example.springsecurity3.hndler;
import com.alibaba.fastjson.JSON;
import com.example.springsecurity3.utils.ResponseResult;
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;
import java.io.PrintWriter;
/**
* 登陆失败,后提示信息
*/
public class LoginFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter pw = response.getWriter();
String json = JSON.toJSONString(ResponseResult.FAIL );
pw.print(json);
pw.flush();
pw.close();
}
}
未登录访问时,提示请先登录
package com.example.springsecurity3.hndler;
import com.alibaba.fastjson.JSON;
import com.example.springsecurity3.utils.ResponseResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 前后端分离项目,用户未登录直接访问系统资源,会被该类拦截
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter pw = response.getWriter();
String json = JSON.toJSONString(ResponseResult.NOTLOGIN );
pw.print(json);
pw.flush();
pw.close();
}
}
有用户名和密码,没有权限时提示请联系管理员
package com.example.springsecurity3.hndler;
import com.alibaba.fastjson.JSON;
import com.example.springsecurity3.utils.ResponseResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 虽然知道用户名和密码,但是你没有权限访问就会拦截呢没有权限的访问操作
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter pw = response.getWriter();
String json = JSON.toJSONString(ResponseResult.NOTAUTH );
pw.print(json);
pw.flush();
pw.close();
}
}
退出成功提示
package com.example.springsecurity3.hndler;
import com.alibaba.fastjson.JSON;
import com.example.springsecurity3.utils.ResponseResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 用户退出操作
*/
public class LogoutSuccess implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter pw = response.getWriter();
String json = JSON.toJSONString(ResponseResult.LOGOUT );
pw.print(json);
pw.flush();
pw.close();
}
}
创建JWT工具类
package com.example.springsecurity3.utils;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import org.springframework.beans.factory.SmartInitializingSingleton;
import java.text.ParseException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
/**
* jet工具类
* 1:创建jwt
* 2:校验jwt是否合法
* 3:返回载荷部分
*/
public class JWTUtils {
public static final String KEY="adbaobcabcnpncinnpcnpnpnc";
//随机生成32位的字符串
//public static final String KEY=UUID.randomUUID().toString
public static String createJWT(String username)throws Exception{
//创建JWT头部
JWSHeader jwsHeader=new JWSHeader.
Builder(JWSAlgorithm.EdDSA.HS256).
type(JOSEObjectType.JWT).build();
//创建载荷,储存用户信息
Map map=new HashMap<>();
map.put("username",username);
Payload payload = new Payload(map);
MACSigner jwsSigner = new MACSigner(KEY);
//3.2创建类将头部和载荷加在一起组成新的字符串
JWSObject jwsObject=new JWSObject(jwsHeader,payload);
//3.3根据秘钥将jwsObject加密
jwsObject.sign(jwsSigner);
return jwsObject.serialize();
}
/**
* 接收用户传入的jwt字符串,根据秘钥进行解码,对比用户传入的jwt是正确的
* 第一步:从页面拿到jwt字符串
* 第二部:将jwt字符串成一个对象
* 第三步:调用verify()方法,传一个秘钥看看能不能解析出来
* JSON.parseObject 将字符串转成对象 JSON.toJSONString 将对象转成字符串
* @param jwt
* @return
* @throws Exception
*/
public static boolean getDecrypt (String jwt)throws Exception{
//把jwt字符串转成jwt对象
JWSObject jwsObject = JWSObject.parse(jwt);
//把jwt对象通过秘钥解密
MACVerifier macVerifier = new MACVerifier(KEY);
//解密
boolean verify = jwsObject.verify(macVerifier);
return verify;
}
/**
* 获取载荷内容
* @param jwt
* @return
* @throws Exception
*/
public static Map getAccount(String jwt) throws Exception {
//获取jet对象
JWSObject parse = JWSObject.parse(jwt);
//获取用户信息
return parse.getPayload().toJSONObject();
}
}