shiro在springboot中一般为统一配置,每个需要的请求都可以用注解拦截和认证。在springcloud中,你可以单拉一个shiro模块,每个模块pom文件引入这个shiro模块,也可以只在网关模块配置shiro。
如果只是实现请求拦截,spring拦截器和注解AOP也方便实现。但本文拉入了shiro,原理是同spirng拦截器实现的,具体如下。
引入shiro/jwt/session实现所需依赖:
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
jwt工具类:
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "xzc.jwt")
public class JwtUtils {
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId+"")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
shiroConfig配置:
@Configuration
public class ShiroConfig {
@Autowired
JwtFilter jwtFilter;
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(defaultWebSecurityManager);
Map<String, String> map = shiroFilterChainDefinition.getFilterChainMap();
// map.put("/user/test", "perms[user:add]"); 这里不用,而采用AuthParams列表。
bean.setFilterChainDefinitionMap(map);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
bean.setFilters(filters);
bean.setLoginUrl("/user/goLogin");
bean.setUnauthorizedUrl("/user/noauth");
return bean;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/test", "jwt");
filterMap.put("/user/test2", "jwt");
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(AccountRealm accountRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(accountRealm);
return securityManager;
}
@Bean
public AccountRealm accountRealm() {
return new AccountRealm();
}
}
AccountRealm配置在本篇中没什么作用,所以public class AccountRealm extends AuthorizingRealm 简单实现一下就行。
实现逻辑在shiroConfig所引入的JwtFilter类,所有要做权限拦截的路由都是在shiroFilterChainDefinition()方法通过filterMap.put("/user/test", "jwt");添加,如果嫌太多也可以用通配符"/**/t"表示所有末尾加"/t"的路径,然后shiroFilterFactoryBean()方法里filters.put("jwt", jwtFilter);实现。
@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
AuthParams authParams;
@Autowired
RestTemplate restTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if (StringUtils.isEmpty(jwt)) {
return null;
}
return new JwtToken(jwt);
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if (StringUtils.isEmpty(jwt)) {
servletResponse.setContentType("0"); //通过返回前端type判断jwt错误类型
return false;
} else {
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
servletResponse.setContentType("1");
throw new ExpiredCredentialsException("token已过期,请重新登录");
}
String requestURI = ((HttpServletRequest) servletRequest).getRequestURI();
String param = authParams.GetPerms().get(requestURI);
String userId = claim.getSubject();
HttpSession httpSession = ((ShiroHttpServletRequest) servletRequest).getSession();
Object userObj = httpSession.getAttribute("user");
Map<String, Object> userMap;
if (userObj == null) {
User user = restTemplate.postForObject("http://user/getbyid", Long.valueOf(userId), User.class);
if (user == null) {
servletResponse.setContentType("2");
return false;
}
String jsonStr = JSONObject.toJSONString(user);
userMap = JSONObject.parseObject(jsonStr, Map.class);
httpSession.setAttribute("user", user);
} else {
userMap = JSONObject.parseObject(userObj.toString(), Map.class);
}
if (param == null) {
return true;
}
if (userMap.get("promission").indexOf(param)==-1) {
servletResponse.setContentType("3");
return false;
}
return true;
}
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result result = Result.error(throwable.getMessage());
String json = JSONUtil.toJsonStr(result);
try {
httpServletResponse.getWriter().print(json);
} catch (IOException ioException) {
}
return false;
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
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;
}
}
@Component
public class AuthParams {
public Map<String,String> GetPerms(){
Map<String,String> map=new HashMap<>();
map.put("/user/test","user:add");
map.put("/order/test2","order:delete");
return map;
}
}
onAccessDenied()判断逻辑:每个登录用户在请求中都会携带一个“Authorization”请求头,如果没有servletResponse.setContentType("0"),作用是约定“0”代表token为空,“1”表示已过期,“2”表示用户不存在,“3”代表没有操作权限。这个ContentType可给前端返回值的头部信息中获得,而实现相关的控制逻辑。
尝试通过httpSession获取当前用户,如果为空则通过token获取用户id,然后通过id重新获取该用户信息,并写入httpSession。
权限列表写在AuthParams里,通过判断当前用户权限列表里是否包含该请求路径权限值,验证是否具有操作权限。
当然也能用角色管理权限,例如map.put("/user/test","admin,student");不过可以用mysql管理,登录时把所有请求路径权限处理成("/user/test","admin,student")map集合,保存在httpSession里。那么判断就要改成:
if(param.indexOf(userMap.get("role"))==-1){
servletResponse.setContentType("3");
return false;
}
方便操作的实现还是第一种,因为让mysql维护请求路径不太现实。没添加在AuthParams里的请求路径直接放行,而当程序员把请求权限写入后,也需要在添加请求权限的页面上添加入库。添加页面包括请求名称/权限值,以及父级路由,这样在编辑角色权限时,也能在权限路由子级下显示细粒度的权限请求。那么未指定父级路由的,可在编辑角色权限页面单独显示即可,全选全不选或点选。注册用户应该有个默认角色,通常只有管理员可以分配角色,用户登录时就可以根据角色加载promission的集合了。