在掌握了shiro以及jwt相关基础知识的前提下
一 实现的功能:
1: jwt结合shrio登录(登录成功后返回对应的token给前端)。
2:shiro对接口进行授权控制。
3:在没有登录的情况下,不允许访问未开放的接口。
4:通过注解的方式对接口进行授权,可以设置相关角色,相关权限才可以操作
特殊说明:CpaToken是前端需要将登陆后返回的token值每次请求都携带在heard头中,用户查询等相关的sql和实体类,本篇博客不包含。另外相关的角色表,和权限表的结构每个公司的结构可能不一样,但是思想是一样的:用户表,角色表,用户角色关系表,权限表,角色权限关系表。
二 导入shiro以及jwt相关的包
io.jsonwebtoken
jjwt
0.9.0
org.apache.shiro
shiro-spring
1.4.0
com.auth0
java-jwt
3.4.0
三 代码片段:
1:自定义的Realm
package com.xtzn.cpa.shiro;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xtzn.cpa.entity.CpaUser;
import com.xtzn.cpa.service.impl.CpaUserServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 org.springframework.beans.factory.annotation.Autowired;
/**
* @author:PSY
* @date:2020/4/15
* @description:
*/
@Slf4j
public class CpaUserRealm extends AuthorizingRealm {
@Autowired
private CpaUserServiceImpl userService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
// 执行授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = JWTUtil.getUsername(principals.toString());
// 查询数据库中此人的权限信息,包括角色名,权限名
CpaUser user = userService.getUser(username, null);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 查询登录者的角色名
simpleAuthorizationInfo.addRoles(user.getRoles());
// 查询登录者的权限信息
simpleAuthorizationInfo.addStringPermissions(user.getPermissions());
return simpleAuthorizationInfo;
}
//执行登录认证的逻辑
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
try {
// SimpleAuthenticationInfo中的参数返回
String token = (String) authenticationToken.getCredentials();
String username = JWTUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token无效!");
}
// 根据用户名查询数据库中的用户相关信息,方便对比token
CpaUser user = userService.getOne(new QueryWrapper().lambda().eq(CpaUser::getUsername, username).eq(CpaUser::getStatus, 1));
// 验证token
if (!JWTUtil.verify(token, user.getId(), user.getPassword())) {
throw new AuthenticationException("账户密码错误!");
}
return new SimpleAuthenticationInfo(token, token, "cpaUserRealm");
} catch (Exception e) {
throw new AuthenticationException("token无效!");
}
}
}
2:实现自定义的jwt过滤器
package com.xtzn.cpa.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author:PSY
* @date:2020/4/16
* @description:
*/
@Slf4j
public class JWTFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
return executeLogin(request, response);
}
/**
* 执行登录的操作
* 检测header里面是否包含CpaToken字段即可
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws AuthenticationException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("CpaToken");
JWTToken jwtToken = new JWTToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
return executeLogin(request, response);
}
// return false ,代表的是没有登录或者token不正确就无法获取到对应的数据,什么都看不了。
// 如果是return true 就算是token不正确也可以看到或者操作数据
return false;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "*");
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
} catch (Exception e) {
response401(response);
}
return false;
}
/**
* 将非法请求跳转到 后续会用到的请求地址
*/
private void response401(ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/user/401");
} catch (IOException e) {
throw new RuntimeException("重定向获取出现异常!");
}
}
}
3:JwtToken
package com.xtzn.cpa.shiro;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @author:PSY
* @date:2020/4/16
* @description:
*/
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;
}
}
4:Jwt工具类相关信息
package com.xtzn.cpa.shiro;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.xtzn.cpa.util.MDUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
/**
* @author:PSY
* @date:2020/4/16
* @description:
*/
@Slf4j
public class JWTUtil {
// 设置为10小时失效的token
private static final long EXPIRE_TIME = 600 * 60 * 1000;
// token存入密码的盐值
private static final String passwordSalt = "wwqr1we!";
public static boolean verify(String token, int userId, String password) {
try {
Claims cpaLoginToken = Jwts.parser().setSigningKey("cpaLoginToken").parseClaimsJws(token).getBody();
String id = cpaLoginToken.getId();
String tokenPassword = (String) cpaLoginToken.get("password" + passwordSalt);
if (userId == Integer.parseInt(id) && !isTokenExpired(cpaLoginToken.getExpiration()) && MDUtil.passwordEncrypt(password + passwordSalt).equals(tokenPassword))
return true;
} catch (Exception exception) {
log.error("token验证失败!");
return false;
}
return false;
}
/**
* @param token
* @return
* @Title: getUsername
* @Description: 通过token获取对应的用户名
* @Author PSY
* @DateTime 2020年4月16日 下午4:42:39
*/
public static String getUsername(String token) {
try {
Claims cpaLoginToken = Jwts.parser().setSigningKey("cpaLoginToken").parseClaimsJws(token).getBody();
return cpaLoginToken.getSubject();
} catch (JWTDecodeException e) {
return null;
}
}
public static String jwtSign(String username, String password, Integer userId) {
//当前时间
long now = System.currentTimeMillis();
//过期时间为1分钟
long exp = now + EXPIRE_TIME;
JwtBuilder builder = Jwts.builder().setId(userId.toString())
.setSubject(username)
//用于设置签发时间
.setIssuedAt(new Date())
// 将密码通过盐值加密,然后放入到token中
.claim("password" + passwordSalt, MDUtil.passwordEncrypt(password + passwordSalt))
//用于设置签名秘钥
.signWith(SignatureAlgorithm.HS256, "cpaLoginToken")
//设置超时的时间
.setExpiration(new Date(exp));
return builder.compact();
}
/**
* @return
* @author PSY
* @date 2020/4/17 10:10
* @接口描述: 判断token是否过期(true过期 , false没有过期), 其实这里是多余的操作,token超时后请求悔自动的失效
* @parmes
*/
public static boolean isTokenExpired(Date ExpirationDate) {
return ExpirationDate.before(new Date());
}
}
最关键的是ShrioConfig的相关配置,注册JwtFilter以及开启shiro的注解配置,这里需要注意一个过滤链的问题,应该将 filterRuleMap.put("/**", "jwt");放在最后。
package com.xtzn.cpa.config;
import com.xtzn.cpa.shiro.CpaUserRealm;
import com.xtzn.cpa.shiro.JWTFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author:PSY
* @date:2020/4/15
* @description:最基本的最关键的配置类
*/
@SpringBootConfiguration
public class ShiroConfig {
//ShiroFilterFactoryBean
@Bean("shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map filterMap = new HashMap<>();
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setSecurityManager(securityManager);
/**
* 自定义url规则
* http://shiro.apache.org/web.html#urls-
*/
Map filterRuleMap = new LinkedHashMap<>();
// 所有的请求通过我们自己的JWT filter
// 访问401和404页面不通过我们的Filter
filterRuleMap.put("/user/401","anon");
filterRuleMap.put("/lua/**", "anon");
filterRuleMap.put("/error", "anon");
filterRuleMap.put("/user/login", "anon");
filterRuleMap.put("/errorResponse", "anon");
filterRuleMap.put("/swagger-ui.html/**", "anon");
filterRuleMap.put("/swagger-ui.html", "anon");
filterRuleMap.put("/webjars/**", "anon");
filterRuleMap.put("/v2/**", "anon");
filterRuleMap.put("/swagger-resources/**", "anon");
// 注意shiro过滤链的顺序
filterRuleMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilterFactoryBean;
}
// 创建DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("cpaUserRealm") CpaUserRealm cpaUserRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
// 关联realm
securityManager.setRealm(cpaUserRealm);
return securityManager;
}
//创建realm
@Bean(name = "cpaUserRealm")
public CpaUserRealm getRealm() {
return new CpaUserRealm();
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
登陆的代码,目的是获取到对应的token
@ApiOperation("用户登录的接口")
@PostMapping(value = "/login", produces = {"application/json;charset=UTF-8"})
public ReturnResult login(@RequestParam("username") String username,
@RequestParam("password") String password) {
try {
JSONObject jsonObject = new JSONObject();
// 根据用户的姓名和加密后的密码查询数据库中符合要求的数据
CpaUser userForHtml = iCpaUserService.getUser(username, password);
if (userForHtml != null) {
// 登陆成功的话生成一个token返回给前端
String token = JWTUtil.jwtSign(username, userForHtml.getPassword(), userForHtml.getId());
// 以下为公司的业务,可忽略
boolean admin = userForHtml.getRoles().contains("admin");
if (admin) {
userForHtml.setUserType(1);
} else {
userForHtml.setUserType(0);
}
userForHtml.setPassword(null);
// 返回用户的信息和token信息
jsonObject.put("OfferToken", token);
jsonObject.put("userInfo", userForHtml);
return ReturnResult.success(ReturnMsg.SUCCESS.getCode(), ReturnMsg.SUCCESS.getMsg(), jsonObject);
}
} catch (Exception e) {
e.printStackTrace();
return ReturnResult.error(ReturnMsg.ERROR.getCode(), e.getMessage());
}
return ReturnResult.error(ReturnMsg.ERROR.getCode(), "登录失败!");
}
四:对接口通过注解进行授权,只需要在接口中增加对应的注解即可,并且添加对应的角色或者权限的信息,具体的信息可以查询相关的信息。
package com.xtzn.cpa.config;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author:PSY
* @date:2020/4/17
* @description:
*/
@SpringBootConfiguration
public class CORSConfiguration {
@Bean
public WebMvcConfigurer CORSConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
//设置是否允许跨域传cookie
.allowCredentials(true)
//设置缓存时间,减少重复响应
.maxAge(3600);
}
};
}
}
测试的结果