Shiro:权限框架,可在C/S下运行。 shiro是一套权限管理框架,包括认证、授权等,在使用时直接写相应的接口(小而简单的Shiro就足够)
jwt:是一个鉴权生成加密token的一个名称。
oauth2.0 :一种权限实现标准,是一种安全的授权框架,提供了一套详细的授权机制。用户或应用可以通过公开的或私有的设置,使用第三方认证和授权。
Shiro 有三大核心组件,即 Subject、SecurityManager 和 Realm。
javax.servlet
javax.servlet-api
4.0.1
org.apache.shiro
shiro-spring-boot-starter
1.8.0
com.auth0
java-jwt
3.18.3
主要功能如下:
public class JwtUtil {
public static final String key = "thisiskey";
//指定一个token过期时间(毫秒)
private static final long EXPIRE_TIME = 1000 * 60 * 10;
/**
* 生成token
*/
public static String createJwtToken(String username, List roleList, List accessList) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(key); //使用密钥进行哈希
// 附带username信息的token
return JWT.create()
.withClaim("username", username)
.withClaim("roles", roleList)
.withClaim("access", accessList)
.withExpiresAt(date) //过期时间
.sign(algorithm); //签名算法
}
/**
* 校验token是否正确
*/
public static boolean verifyToken(String token) {
try {
//根据密钥生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(key);
JWTVerifier verifier = JWT.require(algorithm)
.build();
//效验TOKEN(其实也就是比较两个token是否相同)
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 在token中获取到username信息
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 在token中获取到username信息
*/
public static List getRole(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("roles").asList(String.class);
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 在token中获取到username信息
*/
public static List getAccess(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("access").asList(String.class);
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 判断是否过期
*/
public static boolean isExpire(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().getTime() < System.currentTimeMillis();
}
}
JwtToken类:
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;
}
}
public interface IUserService {
String queryPasswordByName(String userName);
RolePermModel queryRolePermByName(String userName);
}
新建的model类用于用户权限信息(角色和权限)。
public class RolePermModel {
private List roleName;
private List permList;
public List getPermList() {
return Collections.unmodifiableList(permList);
}
}
自定义的Realm需要继承AuthorizingRealm 类,我们需要重写以下两个方法。
doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。
@Slf4j
public class CustomerRealm extends AuthorizingRealm {
@Autowired(required = false)
IUserService iUserService;
// 设置realm的名称
@Override
public void setName(String name) {
super.setName("customRealm");
}
/**
* 大坑!,必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 授权的方法
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("开始授权!");
if (iUserService == null) {
return null;
}
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
RolePermModel rolePermModel = iUserService.queryRolePermByName(principalCollection.getPrimaryPrincipal().toString());
simpleAuthorizationInfo.addRoles(rolePermModel.getRoleName());
simpleAuthorizationInfo.addStringPermissions(rolePermModel.getPermList());
return simpleAuthorizationInfo;
}
/**
* 认证的方法
*
* @param authenticationToken
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
log.info("开始认证!");
if (iUserService == null) {
log.warn("IUserService Is Empty");
return null;
}
JwtToken jwtToken = (JwtToken) authenticationToken;
String token = jwtToken.getCredentials().toString();
String username = JwtUtil.getUsername(token);
//toke过期
if (JwtUtil.isExpire(token)) {
throw new ExpiredCredentialsException();
}
if (!JwtUtil.verifyToken(token)) {
throw new IncorrectCredentialsException();
}
List role = JwtUtil.getRole(token);
List access = JwtUtil.getAccess(token);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
username, token, this.getName());
return simpleAuthenticationInfo;
}
/**
* 这里可以自定义密码比较器
*
* @param credentialsMatcher
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
super.setCredentialsMatcher(credentialsMatcher);
}
}
自定义的过滤器需要继承AuthenticatingFilter,并且需要重写onAccessDenied方法,重写的方法必须执行SecurityUtils.getSubject().login才会执行我们CustomerRealm中的认证的方法(doGetAuthenticationInfo)
public class JwtAuthFilter extends AuthenticatingFilter {
/**
* 获取token
*
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
String requestToken = getRequestToken((HttpServletRequest) servletRequest);
JwtToken jwtToken = new JwtToken(requestToken);
return jwtToken;
}
/**
* 对跨域提供支持
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) 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"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equalsIgnoreCase("OPTIONS")) {
httpServletResponse.setStatus(200);
return false;
}
return super.preHandle(request, response);
}
/**
* 验证token
* 当访问拒绝时是否已经处理了;
* 如果返回true表示需要继续处理;
* 如果返回false表示该拦截器实例已经处理完成了,将直接返回即可。
*
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//完成token登入
//1.检查请求头中是否含有token
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String token = getRequestToken(httpServletRequest);
//2. 如果客户端没有携带token,拦下请求
if (null == token || "".equals(token)) {
Utils.responseToken(servletResponse, "Token无效(authorization不存在)", HttpStatus.UNAUTHORIZED.value());
return false;
}
//3. 如果有,对进行进行token验证
return this.executeLogin(servletRequest, servletResponse);
}
/**
* 执行认证
*
* @param request
* @param response
* @return
* @throws Exception
*/
public boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
JwtToken jwtToken = (JwtToken) createToken(request, response);
try {
SecurityUtils.getSubject().login(jwtToken);
} catch (Exception e) {
if (e.getClass().getName().equalsIgnoreCase(AuthenticationException.class.getName())) {
Utils.responseToken(response, "Token无效,您无权访问该接口!", HttpStatus.UNAUTHORIZED.value());
} else if (e.getClass().getName().equalsIgnoreCase(UnknownAccountException.class.getName())) {
Utils.responseToken(response, "用户名不存在!", HttpStatus.SERVICE_UNAVAILABLE.value());
} else if (e.getClass().getName().equalsIgnoreCase(IncorrectCredentialsException.class.getName())) {
Utils.responseToken(response, "密码错误!", HttpStatus.SERVICE_UNAVAILABLE.value());
} else if (e.getClass().getName().equalsIgnoreCase(ExpiredCredentialsException.class.getName())) {
Utils.responseToken(response, "Token已过期!", HttpStatus.SERVICE_UNAVAILABLE.value());
} else {
Utils.responseToken(response, "其他错误!", HttpStatus.SERVICE_UNAVAILABLE.value());
}
return false;
}
return true;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader("authorization");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isEmpty(token)) {
token = httpRequest.getParameter("authorization");
}
return token;
}
}
Filter Chain 定义说明:
Filter Name | Class |
---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter(所有 url 都都可以匿名访问) |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter(需要认证才能进行访问) |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter(需要权限访问) |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter(需要角色访问) |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
// /hello/add 必须认证,并且角色admin和addqueryrole,并且是add权限可以访问
urlFilterMap.put("/hello/add", "authc,roles[admin,addqueryrole],perms[add]");
// /hello/query 必须认证,并且角色admin或者addqueryrole,并且是add或者query权限可以访问
// authc,roles,perms是shiro内置的过滤器,roles中括号是并且的关系
// rolesOr和permsOr 是我们定义的过滤
urlFilterMap.put("/hello/query", "authc,rolesOr[admin,addqueryrole],permsOr[add,query]");
/**
* 角色或过滤器
*/
public class CustomRolesOrFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
Subject subject = getSubject(request, response);
String[] rolesArray = (String[]) mappedValue;
List rolesList = CollectionUtils.asSet(rolesArray).stream().collect(Collectors.toList());
for (String role : rolesList) {
if (subject.hasRole(role)) {
return true;
}
}
return false;
}
}
/**
* 权限或过滤器
*/
public class CustomPermissionsFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
Subject subject = getSubject(request, response);
String[] rolesArray = (String[]) mappedValue;
List permsList = CollectionUtils.asSet(rolesArray).stream().collect(Collectors.toList());
for (String perms : permsList) {
if (subject.isPermitted(perms)) {
return true;
}
}
return false;
}
}
新建一个接口用于检验密码,登录成功返回token
public class UserController extends MarkController {
@Autowired(required = false)
IUserService iUserService;
@Autowired
CredentialsMatcher credentialsMatcher;
@GetMapping("/login")
public String testCreateToken(@RequestParam String userName, @RequestParam String passWord) {
//从数据库查询密码,
String dbpassword= iUserService.queryPasswordByName(userName);
if (userName == null || secret == null) {
return "user is not exists!";
}
if (!dbpassword.equals(passWord)) {
return "password is error!";
}
RolePermModel rolePermModel = iUserService.queryRolePermByName(userName);
return JwtUtil.createJwtToken(userName, rolePermModel.getRoleName(), rolePermModel.getPermList());
}
}
IFilterService 是我自定新建的抽象类,用于上层抽象具体的业务,自定义过滤器和资源权限的注入。
@Configuration
public class ShiroAutoConfig {
@Autowired(required = false)
IFilterService iFilterService;
//1创建shiroFilter 负责拦截请求
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filterMap = new HashMap<>();
filterMap.put("jwtfilter", new JwtAuthFilter());
// 角色或关系
filterMap.put("rolesOr", new CustomRolesOrFilter());
// 权限或关系
filterMap.put("permsOr", new CustomPermissionsoOrFilter());
Map temp = iFilterService == null ? null : iFilterService.createFilterMap();
if (temp != null) {
filterMap.putAll(temp);
}
//自定义过滤器
shiroFilterFactoryBean.setFilters(filterMap);
//配置系统受限资源
Map urlFilterMap = new LinkedHashMap<>();
urlFilterMap.put("/login", "anon");
// /hello/add 必须认证,并且角色admin和addqueryrole,并且是add权限可以访问
urlFilterMap.put("/hello/add", "authc,roles[admin,addqueryrole],perms[add]");
// /hello/query 必须认证,并且角色admin或者addqueryrole,并且是add或者query权限可以访问
// authc,roles,perms是shiro内置的过滤器,roles中括号是并且的关系
// rolesOr和permsOr 是我们定义的过滤
urlFilterMap.put("/hello/query", "authc,rolesOr[admin,addqueryrole],permsOr[add,query]");
urlFilterMap.put("/hello/delete", "rolesOr[admin,delrole],permsOr[delete]");
// 对除了anon的过滤器,全部增加WT的过滤器
urlFilterMap.entrySet().forEach(kv -> {
if (kv.getValue().contains("anon")) {
return;
}
if (!kv.getValue().contains("jwtfilter")) {
kv.setValue("jwtfilter," + kv.getValue());
}
});
// 全部接口使用JWT的过滤器
urlFilterMap.put("/**", "jwtfilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(urlFilterMap);
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuthorization");
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean("securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("realm") Realm realm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
defaultWebSecurityManager.setSubjectDAO(defaultSubjectDAO());
return defaultWebSecurityManager;
}
/**
* 关闭shiro的session(无状态的方式使用shiro),shiro 默认带缓存,
* 所以第一次认证之后就不会触发认证了
*
* @return
*/
private DefaultSubjectDAO defaultSubjectDAO() {
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
return subjectDAO;
}
//3.创建自定义realm
@Bean("realm")
public Realm getRealm() {
CustomerRealm customerRealm = new CustomerRealm();
return customerRealm;
}
@Bean
@ConditionalOnClass(RestController.class)
public UserController tokenController() {
UserController tokenController = new UserController();
return tokenController;
}
@Bean
@ConditionalOnClass(RestController.class)
public ExceptionController exceptionController() {
ExceptionController exceptionController = new ExceptionController();
return exceptionController;
}
@Bean
HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//加密类型(+)
hashedCredentialsMatcher.setHashAlgorithmName("sha-256");
//#加密迭代次数(+)
hashedCredentialsMatcher.setHashIterations(1000);
//#true=hex格式 false=base64(+)
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(false);
return hashedCredentialsMatcher;
}
}
package com.lx.shirojwt.controller;
import com.lx.shirojwt.util.ShiroConstants;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
@RestController
@RestControllerAdvice
public class ExceptionController extends MarkController {
@GetMapping("/noAuthorization")
public ResponseEntity noAuthorization(HttpServletRequest request) {
return new ResponseEntity("无权限访问该接口!", HttpStatus.FORBIDDEN);
}
@ExceptionHandler({AuthorizationException.class})
public ResponseEntity authorizationException(Exception exception, HttpServletRequest request) {
return new ResponseEntity("无权限访问!" + request.getRequestURI(), HttpStatus.FORBIDDEN);
}
}
无token字段:
token过期:
1.对于JWT每次请求都会进行认证,首先先验证token的有效期,然后校验token的合法性,最后解析token获取用户信息。
2.目前没有使用缓存,每次鉴权都要去数据库查Role、查Permissions。