SpringBoot整合JWT+shiro
前段时间写了个前后端分离的项目,被跨域的问题折磨了,前几天学了JWT,和shiro整合后就可以更方便了。
不过自己技术浅薄,在网上找了会后发现一个挺不错的博客,看了一遍后写完了。
放上链接:
https://blog.csdn.net/u010606397/article/details/104110093
1、shiro的工作流程
在整合这两个之前我们需要清楚shiro+jwt的工作流程,如下:
1、AccessControlFilter#onPreHandle()
2、AuthenticationFilter#isAccessAllowed(),此方法仅判断当前的subject(用户)是否已经认证,结果subject.authenticated是未认证。
3、JwtFilter#onAccessDenied(),用户未认证,此方法被调用。
4、JwtFilter#executeLogin(), 使用请求头Authorization生成JwtToken,然后执行subject.login(JwtToken);
5、AuthenticatingRealm#getAuthenticationInfo(),通过JwtToken查询缓存中的AuthenticationInfo
6、缓存中没有AuthenticationInfo,进入 JwtRealm#doGetAuthenticationInfo(),校验token合法性,并生成SimpleAuthenticationInfo。
7、DefaultWebSubjectFactory#createSubject,使用SimpleAuthenticationInfo中的principals生成一个已认证的subject,用户认证就完成了。
授权流程是这样:
1、AccessControlFilter#onPreHandle()
2、PermissionsAuthorizationFilter#isAccessAllowed(),判断subject是否具备接口权限。
3、AuthorizingRealm#getAuthorizationInfo(),获取授权缓存。
4、没有授权缓存,通过JwtRealm#doGetAuthorizationInfo()获取授权信息。
有了这个我们就开始写代码了。
2、JWTUtils
首先需要有一个JWT的工具类,我这个工具类写的很不完善,临时用一下。
public class JWTUtils {
private static final String SIGN = "!@a$Df^a8b)%";
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7);
JWTCreator.Builder builder = JWT.create();
builder.withExpiresAt(instance.getTime());
for (String s : map.keySet()) {
builder.withClaim(s,map.get(s));
}
String token = builder.sign(Algorithm.HMAC256(SIGN));
return token;
}
public static DecodedJWT verify(String token){
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
/*public static DecodedJWT getTokenInfo(String token){
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}*/
public static String getUsername(String token){
String username = String.valueOf(JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token).getClaim("username"));
return username;
}
}
2、JwtToken
创建一个JwtToken用来替代AuthenticationToken,如下:
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return this.token;
}
//因为token里面有信息了,这个直接返回空就行
@Override
public Object getCredentials() {
return "";
}
}
3、Filter
根据上面的流程,我们需要两个filter,一个BasicHttpAuthenticationFilter,一个PermissionsAuthorizationFilter。
首先创建一个JwtFilter,继承BasicHttpAuthenticationFilter,如下:
//创建一个JwtFilter继承BasicHttpAuthenticationFilter
public class JwtFilter extends BasicHttpAuthenticationFilter {
/*
重写createToken,onAccessDenied,executeLogin三个方法
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
//获取请求头中Authorization的值
String authorization = getAuthzHeader(request);
//用获取到的值创建token对象
return new JwtToken(authorization);
}
//身份认证未通过
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
boolean r;
String authorization = getAuthzHeader(request);
HashMap<String, Object> map = new HashMap<>();
//判断authorization是否为空
if (authorization == null||authorization == ""){
map.put("state",false);
map.put("massage","没有token");
//没有token,返回false,登录页面虽然没有token,但是不会被拦截
r = false;
response.setCharacterEncoding("UTF-8");
response.getWriter().println(JSONObject.toJSON(map));
} else {
r = executeLogin(request, response);
}
return r;
}
//执行登录操作
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = this.createToken(request, response);
HashMap<String, Object> map = new HashMap<>();
response.setCharacterEncoding("UTF-8");
if(token == null){
map.put("state",false);
map.put("massage","未登录");
return false;
} else{
try{
Subject subject = this.getSubject(request, response);
subject.login(token);
return this.onLoginSuccess(token,subject,request,response);
} catch (Exception e){
map.put("state",false);
map.put("massage","用户认证异常");
e.printStackTrace();
response.getWriter().println(JSONObject.toJSON(map));
response.getWriter().close();
return false;
}
}
}
}
用户访问时,被过滤器拦截下来后因为没有认证,调用onAccessDenied方法,如果没有附带token,就返回没有token,访问结束。如果附带了token,则执行executeLogin方法。
然后创建一个CustomPermissionsAuthorizationFilter继承PermissionsAuthorizationFilter,如下:
public class CustomPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {
/**
* 用户无权访问url时,此方法会被调用
* 默认实现为org.apache.shiro.web.filter.authz.AuthorizationFilter#onAccessDenied()
* 覆盖父类的方法,返回自定义信息给前端
*
* 接口doc上说:
* AuthorizationFilter子类(权限授权过滤器)的onAccessDenied()应该永远返回false,那么在onAccessDenied()内就必然要发送response响应给前端,不然前端就收不到任何数据
* AuthenticationFilter、AuthenticatingFilter子类(身份认证过滤器)的onAccessDenied()的返回值则没有限制
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response){
HashMap<String, Object> map = new HashMap<>();
map.put("state",false);
map.put("massage","权限不足");
try {
response.getWriter().println(JSONObject.toJSONString(map));
response.getWriter().close();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
4、Realm
public class JwtRealm extends AuthorizingRealm {
//因为是熟悉流程,所以授权就没写
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getPrincipal();
try{
//检查token是否有效,如果有效就创建一个subject
JWTUtils.verify(token);
return new SimpleAuthenticationInfo(token, "",this.getName());
} catch (Exception e){
e.printStackTrace();
return null;
}
}
// subject.login(token)方法中的token是JwtToken时,调用此Realm的doGetAuthenticationInfo
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
}
5、Config
最后需要一个ShiroConfig来配置好上面的过滤器和Realm
@Configuration
public class ShiroConfig {
@Bean
public JwtRealm jwtRealm(){
return new JwtRealm();
}
@Bean("mySecurityManager")
public DefaultWebSecurityManager securityManager(@Qualifier("jwtRealm") JwtRealm jwtRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(jwtRealm);
//禁止session持久化存储
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("mySecurityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
//添加filter,factoryBean.getFilters()获取到的是引用,可直接添加值
factoryBean.getFilters().put("jwt", new JwtFilter());
factoryBean.getFilters().put("customPerms", new CustomPermissionsAuthorizationFilter());
/**
* factoryBean.getFilterChainDefinitionMap();默认是size=0的LinkedHashMap
*/
Map<String, String> definitionMap = factoryBean.getFilterChainDefinitionMap();
/**
* definitionMap是一个LinkedHashMap,是一个链表结构
* put的顺序很重要,当/open/**匹配到请求url后,将不再检查后面的规则
* 官方将这种原则称为 FIRST MATCH WINS
* https://waylau.gitbooks.io/apache-shiro-1-2-x-reference/content/III.%20Web%20Applications/10.%20Web.html
*/
/**
* 由于禁用了session存储,shiro不会存储用户的认证状态,所以在接口授权之前要先认证用户,不然CustomPermissionsAuthorizationFilter不知道用户是谁
* 实际项目中可以将这些接口权限规则放到数据库中去
*/
/*definitionMap.put("/role/permission/edit", "jwt, customPerms["+userService.getRolePermisssionEdit().getName()+"]");*/
// 前面的规则都没匹配到,最后添加一条规则,所有的接口都要经过com.example.shirojwt.filter.JwtFilter这个过滤器验证
definitionMap.put("/login", "anon");
definitionMap.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(definitionMap);
return factoryBean;
}
}
这次的学习让我对shiro有了更深刻的理解,不再是像以前一样连用都用不太好。所以说经常看代码还是很有用的。