整体思路主要是利用shiro的鉴权机制,自定义鉴权的方法:
1、登录接口,验证登录信息后,通过JWTUtil生成token,通过JWTtoken对象(实现AuthenticationToken中接口)存入subject中
2、接口拦截逻辑,通过shiroConfig的shiroFilter确定匹配规则,在匹配规则上匹配访问的路径需要走自定义的JwtFilter(关键代码filterChainDefinitionMap.put("/**", "jwt");)
3、自定义的JwtFiler主要是为了获取token并交由realm进行验证
4、通过自定义的CustomRealm进行token的解码验证
这里涉及到三点特殊的说明:
1、因为jwt体系禁用了shiro的session体系,所以注销不能通过subject.logout()的方式进行注销,而jwt本身的token值是不能手动销毁的,这里提供一个解决的思路:注销时建立token黑名单,token正式解码验证前先进行黑名单验证。具体代码属于业务代码就不在本篇文章罗列了。
2、验证失败时使用自定义异常的方式进行处理,当系统发生异常时会自动直接返回前端异常信息,相关代码会罗列。
3、权限认证逻辑跟shiro的权限认证逻辑一致(见我的另外一篇《springboot整合shiro完成基本的登录验证》)
应用依赖
org.apache.shiro
shiro-spring
1.3.2
io.jsonwebtoken
jjwt
0.9.1
com.auth0
java-jwt
3.4.0
1、登录接口
package com.example.xxljobdemo.controller;
import com.example.xxljobdemo.util.JwtUtill;
import com.example.xxljobdemo.vo.JwtToken;
import org.apache.shiro.SecurityUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
/**
* 登陆
*
* @param username 用户名
* @param password 密码
*/
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(String username, String password) {
//username、password验证代码略。。。
String token= JwtUtill.sign(username,password);
SecurityUtils.getSubject().login(new JwtToken(token));
return token;
}
}
引用JwtUtill
package com.example.xxljobdemo.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.UUID;
public class JwtUtill {
private static final long EXPIRE_TIME = 60 * 60 * 1000;
/**
* 校验token是否正确
*
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
//根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
//效验TOKEN
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户名
*/
public static Integer getUserId(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("userId").asInt();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 获得tokenId
*
* @return uuid
*/
public static String getTokenId(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getId();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 获取token过期时间
*
* @return 过期时间
*/
public static Date getExpiresAt(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 获取token签发时间
*
* @return 签发时间
*/
public static Date getIssuedAt(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getIssuedAt();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 生成签名
*
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
String jwtId = UUID.randomUUID().toString();
// 附带username信息
return JWT.create()
.withJWTId(jwtId)
.withClaim("username", username)
.withExpiresAt(date)
.withIssuedAt(new Date())
.sign(algorithm);
}
public static void main(String[] args) {
String token = sign("aaa", "123456");
System.out.println("token" + token);
System.out.println(getTokenId(token));
System.out.println(getUserId(token));
System.out.println(getUsername(token));
System.out.println(getIssuedAt(token));
System.out.println(getExpiresAt(token));
System.out.println(verify(token, "aaa", "123456"));
}
}
引用的JwtToken
package com.example.xxljobdemo.vo;
import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
2、ShiroConfig,配置securityManager和shiroFilter规则
package com.example.xxljobdemo.config;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();
//session管理
// webSecurityManager.setSessionManager(sessionManager());
//realm管理
webSecurityManager.setRealm(realm());
//缓存管理
webSecurityManager.setCacheManager(new MemoryConstrainedCacheManager());
//使用ehcache
// EhCacheManager ehCacheManager = new EhCacheManager();
// ehCacheManager.setCacheManager(getEhCacheManager());
// webSecurityManager.setCacheManager(ehCacheManager);
//redis实现
// webSecurityManager.setCacheManager(redisCacheManager());
//关闭session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
webSecurityManager.setSubjectDAO(subjectDAO);
return webSecurityManager;
}
// @Bean
// public RedisCacheManager redisCacheManager() {
//
// RedisManager redisManager = new RedisManager();
// redisManager.setHost("localhost:6379");
// redisManager.setDatabase(1);
// redisManager.setTimeout(5000);
redisManager.setPassword();
//
// RedisCacheManager redisCacheManager = new RedisCacheManager();
// redisCacheManager.setRedisManager(redisManager);
// return redisCacheManager;
// }
//
// @Bean
// public CacheManager getEhCacheManager() {
// EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
// ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("classpath:org/apache/shiro/cache/ehcache/ehcache.xml"));
// return ehCacheManagerFactoryBean.getObject();
// }
@Bean
public Realm realm() {
CustomRealm shiroRealm = new CustomRealm();
return shiroRealm;
}
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
// 设置拦截器
Map filterChainDefinitionMap = new LinkedHashMap<>();
//开放登陆接口
filterChainDefinitionMap.put("/login", "anon");
//开放注销接口
filterChainDefinitionMap.put("/logout", "anon");
//user权限
filterChainDefinitionMap.put("/user/**", "roles[user]");
//admin权限
filterChainDefinitionMap.put("/admin/**", "roles[admin]");
//其余接口一律拦截,走自定义拦截器jwt
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
HashMap myFIleter = new HashMap<>();
myFIleter.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(myFIleter);
return shiroFilterFactoryBean;
}
}
3、自定义的过滤器JWTFilter
package com.example.xxljobdemo.config;
import com.example.xxljobdemo.vo.JwtToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class JWTFilter extends BasicHttpAuthenticationFilter {
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String token = ((HttpServletRequest) request).getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new MyprojectException("token不能为空");
}
executeLogin(request, response);
return true;
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new MyprojectException("token不能为空");
}
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("登录失败");
return super.onAccessDenied(request, response);
}
}
4、自定义CustomRealm
package com.example.xxljobdemo.config;
import com.example.xxljobdemo.util.JwtUtill;
import com.example.xxljobdemo.vo.JwtToken;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.HashSet;
import java.util.Set;
public class CustomRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 获取身份验证信息
* Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
*
* @param authenticationToken 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————身份认证方法————");
String tokenStr=(String) authenticationToken.getPrincipal();
String username = JwtUtill.getUsername(tokenStr);
System.out.println("登录的用户:"+username);
// 模拟通过用户名admin从数据库获取密码123
String password ="";
if("admin".equals(username)){
password ="123";
}
if(JwtUtill.verify(tokenStr,username,password)){
System.out.println("登录成功");
}else {
throw new UnknownAccountException("用户名密码不正确");
}
return new SimpleAuthenticationInfo(tokenStr, tokenStr, getName());
}
/**
* 获取授权信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("————权限认证————");
String username = JwtUtill.getUsername(principalCollection.toString());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 模拟通过用户名admin从数据库获取权限admin
String role ="";
if("admin".equals(username)){
role ="admin";
}
Set set = new HashSet<>();
//需要将 role 封装到 Set 作为 info.setRoles() 的参数
set.add(role);
//设置该用户拥有的角色
info.setRoles(set);
return info;
}
}
5、自定义异常MyprojectException和统一异常捕捉返回的controller
package com.example.xxljobdemo.config;
/**
* @author
* @Description: 自定义业务异常类
* @date
*/
public class MyprojectException extends RuntimeException {
/**
* 错误编码
*/
private String code;
public MyprojectException() {
super();
}
public MyprojectException(String message) {
super(message);
}
public MyprojectException(String code, String message) {
super(message);
this.code = code;
}
public MyprojectException(Throwable cause) {
super(cause);
}
public MyprojectException(String message, Throwable cause) {
super(message, cause);
}
public MyprojectException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
@Override
public String getMessage() {
return super.getMessage();
}
@Override
public String toString() {
return this.code + ":" + this.getMessage();
}
}
package com.example.xxljobdemo.controller;
import com.example.xxljobdemo.config.MyprojectException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@ControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public String handleException(RuntimeException e) {
return "运行异常:"+e.getMessage();
}
@ExceptionHandler(MyprojectException.class)
@ResponseBody
public String doBusinessException(Exception e) {
return "运行异常:"+e.getMessage();
}
}
测试:通过http://172.16.130.7:8081/login?username=zhangsan&password=123获取token
把获取的tonken放在请求头“Authorization”中,访问http://172.16.130.7:8081/test接口