Shiro+JWT实现前后端分离登录验证
- 导入相关的jar包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.16.0</version>
</dependency>
注意:shiro整合JWT导包不能导shiro-spring-boot-web-starter,会报错
- 创建一个用来生成token的工具类
public class JWTUtils {
public static final long EXPIRE = 1000 * 60 * 60 * 24; //过期时间一天
public static final String SECRET = "ABCDEFGH";//这里设置你的盐,至关重要,不要透露给其他人
public static String create(Map<String, String> map) {
JWTCreator.Builder builder = JWT.create();
map.forEach((K, V) -> {
builder.withClaim(K, V); //遍历map将用户数据放入负载中
});
Date now = new Date();
Date expires = new Date(now.getTime() + EXPIRE);
builder.withIssuedAt(now); //设置创建时间
builder.withExpiresAt(expires); //设置过期时间
String uuid = UUID.randomUUID().toString().replaceAll("-",""); //通过UUID生成一个唯一标识
builder.withJWTId(uuid); //设置唯一标识
return builder.sign(Algorithm.HMAC256(SECRET));
}
public static boolean verify(String token) {
try {
JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token); // 如果验证通过,则不会把报错,否则会报错
return true;
} catch (TokenExpiredException e) {//令牌过期异常
return false;
} catch (SignatureVerificationException e) {//签名不一致异常
return false;
} catch (AlgorithmMismatchException e) {//算法不匹配异常
return false;
} catch (InvalidClaimException e){//失效的payload异常
return false;
}catch (Exception e) {
return false;
}
}
public static DecodedJWT getToken(String token) {
//解析传来的字符串
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
}
}
- 封装一个类,继承shiro中的token,里面放token字符串信息
//这个就类似UsernamePasswordToken
//不需要用户名和密码,因为需要的信息以及放在token中了,只要对token进行判断即可
public class JwtToken implements AuthenticationToken {
private String jwt;
public JwtToken(String jwt) {
this.jwt = jwt;
}
@Override//类似是用户名
public Object getPrincipal() {
return jwt;
}
@Override//类似密码
public Object getCredentials() {
return jwt;
}
//返回的都是jwt
}
- 创建一个过滤器,用于拦截请求,拿到请求头中的token,交给shiro的realm做处理
public class JWTFilter extends AccessControlFilter {
/*
* 1. 返回true,shiro就直接允许访问url
* 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
* */
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
//这里先让它始终返回false来使用onAccessDenied()方法
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//从请求头中获取token
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("token");
JwtToken jwtToken = new JwtToken(jwt);
/*
* 下面就是固定写法
* */
try {
// 委托 realm 进行登录认证
//所以这个地方最终还是调用JwtRealm进行的认证
getSubject(servletRequest, servletResponse).login(jwtToken);
//也就是subject.login(token)
} catch (Exception e) {
e.printStackTrace();
//抛出任何异常都调用错误方法
onLoginFail(servletResponse);
//调用下面的方法向客户端返回错误信息
return false;
}
return true;
//执行方法中没有抛出异常就表示登录成功
}
//登录失败时默认返回 -1 状态码
private void onLoginFail(ServletResponse response) throws IOException {
//返回json给前端
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json; charset=utf-8");
httpResponse.getWriter().write("{\"success\":-1}");
}
}
- 在realm中,对过滤器传来的token进行判断
public class JwtRealm extends AuthorizingRealm {
@Autowired
private IAdministratorService adminService;
/*
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
* */
@Override
public boolean supports(AuthenticationToken token) {
//这个token就是从JWTFilter过滤器中传入的jwtToken
return token instanceof JwtToken;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//根据业务自行授权
return null;
}
//认证
//这个token就是从过滤器中传入的token
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwt = (String) token.getPrincipal();
if (jwt == null) {
throw new NullPointerException("token 不允许为空");
}
//判断
if (!JWTUtils.verify(jwt)) {//判断token是否正确
throw new UnknownAccountException();
}
//下面是验证这个admin是否是真实存在的
DecodedJWT decodedJWT = JWTUtils.getToken(jwt);//解析token
String username = decodedJWT.getClaim("username").asString();//判断数据库中username是否存在
QueryWrapper<Administrator> administratorQueryWrapper = new QueryWrapper<>();
administratorQueryWrapper.eq("user_name",username);
Administrator dbAdmin = adminService.getOne(administratorQueryWrapper);
if (dbAdmin == null){
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(jwt,jwt,getName());
//这里返回的是类似账号密码的东西,但是jwtToken都是jwt字符串。不需要进行账号密码的比对,因为上面对token已经进行判断了,token被修改了是无法进入的
}
}
- 配置shiro一些环境
//springBoot整合jwt实现认证有三个不一样的地方,对应下面abc
@Configuration
public class ShiroConfig {
@Bean
public Realm realm() {
return new JwtRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
/*
* b
*/
// 关闭 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
/*
* c. 添加jwt过滤器,并在下面注册
* 也就是将jwtFilter注册到shiro的Filter中
* 指定除了login和logout之外的请求都先经过jwtFilter
* */
Map<String, Filter> filterMap = new HashMap<>();
//这个地方其实另外两个filter可以不设置,默认就是
filterMap.put("jwt", new JWTFilter());
shiroFilter.setFilters(filterMap);
// 拦截器
Map<String, String> filterRuleMap = new LinkedHashMap<>();
//anon代表不拦截
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
filterRuleMap.put("/queryProcess","anon");//放行此请求
filterRuleMap.put("/**", "jwt");//所有请求都拦截
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
}
- 在controller中进行登录判断,并返回token给前端
@PostMapping("/login")
public JSONObject login(@RequestBody String param) {
JSONObject json = JSONObject.parseObject(param);
if (json==null){
json.put("success",0);
return json;
}
String username = json.getString("username");
String password = json.getString("password");
json.clear();
if (username==null||password==null){
json.put("success",0);
return json;
}
QueryWrapper<Administrator> adminQueryWrapper = new QueryWrapper<>();
adminQueryWrapper.eq("user_name",username);
adminQueryWrapper.eq("password",password);
Administrator dbAdmin = administratorService.getOne(adminQueryWrapper);
if (dbAdmin==null){
json.put("success",0);
return json;
}
Map<String, String> chaim = new HashMap<>();
chaim.put("username", dbAdmin.getUserName());
String token = JWTUtils.create(chaim);
json.put("success",1);
json.put("username",username);
json.put("token",token);
return json;
}
@RequestMapping("/testdemo")
public ResponseEntity<String> testdemo() {
return ResponseEntity.ok("我爱蛋炒饭");
}
在权限认证那一块,可以根据自己的业务进行设置和判断