简介:
Apache Shiro 是 Java 的一个安全(权限)框架。
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存 等。
Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛,而且Shiro的API也是非常简单;其基本功能点如下图所示:
[图片上传失败...(image-fb88c2-1583464531431)]
Authentication:身份认证/登录,验证用户是不是拥有相应的身份,例如账号密码登陆;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
shiro自带的拦截器
默认拦截器名 | 拦截器类 | 说明(括号里的表示默认值) |
---|---|---|
认证相关: | ||
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 基于表单的拦截器;如 “/**=authc ”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure); |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | Basic HTTP 身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application); |
logout | org.apache.shiro.web.filter.authc.LogoutFilter | 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/); 示例 “/logout=logout” |
anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例 “/static/**=anon” |
授权相关: | ||
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例 “/admin/**=roles[admin]” |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例 “/user/**=perms["user:create"]” |
其他: | ||
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | 不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常; |
shiro认证流程
- subject(主体)请求认证,调用subject.login(token)
- SecurityManager (安全管理器)执行认证
- SecurityManager通过ModularRealmAuthenticator进行认证。
- ModularRealmAuthenticator将token传给realm,realm根据token中用户信息从数据库查询用户信息(包括身份和凭证)
- realm如果查询不到用户给ModularRealmAuthenticator返回null,ModularRealmAuthenticator抛出异常(用户不存在)
- realm如果查询到用户给ModularRealmAuthenticator返回AuthenticationInfo(认证信息)
- ModularRealmAuthenticator拿着AuthenticationInfo(认证信息)去进行凭证(密码)比对。如果一致则认证通过,如果不致抛出异常(凭证错误)。
shiro授权流程
- 对subject进行授权,调用方法isPermitted("")或者hasRole("")
- SecurityManager执行授权,通过ModularRealmAuthorizer执行授权
- ModularRealmAuthorizer执行realm(自定义的CustomRealm)从数据库查询权限数据调用realm的授权方法:doGetAuthorizationInfo
- realm从数据库查询权限数据,返回ModularRealmAuthorizer
- ModularRealmAuthorizer调用PermissionResolver进行权限串比对
- 如果比对后,isPermitted中"permission串"在realm查询到权限数据中,说明用户访问permission串有权限,否则没有权限,抛出异常。
实际应用
最近在项目中使用到shiro了,是用jwt做无状态登录,Redis做授权的缓存。按步骤记录一下
首先应该明白的是,shiro的默认配置是通过cookie和自带的session来实现缓存和Remember Me这些功能,如果要做无状态应用,应该禁止shiro的session功能。
整个项目分为账号密码登录和jwt登录两种方式,所以需要至少两种Realm,多Realm的认证策略有以下三种
[1]AtLeastOneSuccessfulStrategy
如果一个(或更多)验证成功,则整体的尝试被认为是成功的。如果没有一个验证成功,则整体失败。说白了就是,至少有一个Realm的验证是成功的算才认证通过,否则认证失败。
[2]FirstSuccessfulStrategy
第一个Realm成功验证返回的信息将被使用,其他的Realm将被忽略。如果没有一个Realm验证成功,则整体失败,和第一个的区别就在于,AtLeastOneSuccessfulStrategy是将所有的认证信息都返回,而FirstSuccessfulStrategy认定一个成功则返回。
[3]AllSuccessfulStrategy
所有配置的Realm都必须验证成功才算认证通过,否则认证失败。
在这里使用AtLeastOneSuccessfulStrategy比较好。使用ModularRealmAuthenticator后,如果出现异常,shiro并没有抛出具体异常,而是捕获后重新抛出整体没有认证成功的异常,如果想要抛出具体的异常,需要继承ModularRealmAuthenticator,重写doMultiRealmAuthentication方法将异常抛出。
首先应该做登录的realm,使用数据库校验就可以判断登录的用户名密码是否正确。
其次是jwt登录校验,shiro并没有有关jwt的拦截器和校验器,都需要自己重写。
全部配置完成后,理应对授权做缓存,使用Redis做缓存。
代码
ShiroConfig:
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, UserService userService){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("tokenFilter",createAuthFilter(userService));
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
map.put("/login", "noSessionCreation,anon");
// 先测试,后面再换
//map.put("/insuranceType/list", "noSessionCreation,tokenFilter,roles[member]");
map.put("/**", "noSessionCreation,anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator(){
DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
@Bean
public DefaultWebSecurityManager getSecurityManager(JwtShiroRealm jwtRealm, DbShiroRealm dbShiroRealm, CacheManager cacheManager){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealms(Arrays.asList(jwtRealm, dbShiroRealm));
defaultWebSecurityManager.setCacheManager(cacheManager);
//扩展父类原方法,捕获原始异常
MultiRealmAuthenticator authenticator = new MultiRealmAuthenticator();
//设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
authenticator.setRealms(Arrays.asList(jwtRealm, dbShiroRealm));
//设置多个realm认证策略,一个成功即跳过其它的
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
defaultWebSecurityManager.setAuthenticator( authenticator );
return defaultWebSecurityManager;
}
@Bean("cacheManager")
public CacheManager cacheManager(RedisTemplate redisTemplate){
CacheManager cacheManager = new RedisCacheManager();
((RedisCacheManager) cacheManager).setCache(new RedisCache(redisTemplate));
return cacheManager;
}
//注意不要加@Bean注解
protected JwtAuthFilter createAuthFilter(UserService userService){
return new JwtAuthFilter(userService);
}
}
MultiRealmAuthenticator:
@Slf4j
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = this.getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
Iterator var5 = realms.iterator();
AuthenticationException authenticationException = null;
while(var5.hasNext()) {
Realm realm = (Realm)var5.next();
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable var11) {
t = var11;
authenticationException = (AuthenticationException)var11;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, var11);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
//增加此逻辑,只有authenticationException不为null,则表示有Realm较验到了异常,则立即中断后续Realm验证直接外抛
if (authenticationException != null){
throw authenticationException;
}
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
DbRealm:
@Component
public class DbShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
public DbShiroRealm() {
this.setCredentialsMatcher((AuthenticationToken token, AuthenticationInfo info) -> {
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//要验证的明文密码
String plaintext = new String(userToken.getPassword());
//数据库中的加密后的密文
String hashed = info.getCredentials().toString();
return BCrypt.checkpw(plaintext, hashed);
});
this.setCachingEnabled(true);
this.setAuthorizationCachingEnabled(true);
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken userpasswordToken = (UsernamePasswordToken)token;
String username = userpasswordToken.getUsername();
User checkUser = new User();
checkUser.setState(UserState.NORMAL.getState());
// 通过Email登录
if (Validator.isEmail(username)){
checkUser.setEmail(username);
}else if(Validator.isIdCard(username)){
// 通过身份证登录
checkUser.setIdCard(username);
}else if(Validator.isPhone(username)){
//通过手机号登录
checkUser.setPhone(username);
}else {
// 通过用户名登录;修改用户名时必须做限定
checkUser.setUsername(username);
}
User user = userService.getUserByRules(checkUser);
if(user == null)
throw new AuthenticationException("用户名或者密码错误");
return new SimpleAuthenticationInfo(user, user.getPassword(),getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principals.getPrimaryPrincipal();
user.setRoles(roleService.findRolesByUserId(user.getId()));
Set<Role> roles = user.getRoles();
List<String> roleNames = roles.stream().map(Role::getName).collect(Collectors.toList());
simpleAuthorizationInfo.addRoles(roleNames);
return simpleAuthorizationInfo;
}
}
jwtRealm:
@Component
public class JwtShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
private JwtUtil jwtUtil;
@Autowired
public JwtShiroRealm(JwtUtil jwtUtil){
this.jwtUtil = jwtUtil;
this.setCachingEnabled(true);
this.setAuthorizationCachingEnabled(true);
this.setCredentialsMatcher(new JwtCredentialsMatcher(jwtUtil));
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) authcToken;
String token = jwtToken.getToken();
Optional.ofNullable(token).orElseThrow(()-> new AuthenticationException(ResponseCode.NEED_LOGIN.getDesc()));
Map<String, String> map;
// 校验
try{
map = jwtUtil.verifyToken(token);
}catch (TokenExpiredException e){
throw new AuthenticationException(ResponseCode.TOKEN_EXPIRES.getDesc());
}catch (Exception e){
throw new AuthenticationException("token无效,请重新登陆");
}
String host = map.get("host");
if (host == null || !host.equals(jwtToken.getHost())){
throw new AuthenticationException("token地区错误,请重新登录");
}
int id = Integer.parseInt(map.get("id"));
User user = userService.getUserById(id);
if(user == null)
throw new AuthenticationException("token过期,请重新登录");
return new SimpleAuthenticationInfo(user, null, getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("进行数据库权限读取");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principals.getPrimaryPrincipal();
user.setRoles(roleService.findRolesByUserId(user.getId()));
Set<Role> roles = user.getRoles();
List<String> roleNames = roles.stream().map(Role::getName).collect(Collectors.toList());
simpleAuthorizationInfo.addRoles(roleNames);
return simpleAuthorizationInfo;
}
}
jwtFilter:
@Slf4j
public class JwtAuthFilter extends AuthenticatingFilter {
private static final int tokenRefreshInterval = 300;
private String tokenError = "";
private UserService userService;
public JwtAuthFilter(UserService userService){
this.userService = userService;
this.setLoginUrl("/login");
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) //对于OPTION请求做拦截,不做token校验
return false;
return super.preHandle(request, response);
}
@Override
protected void postHandle(ServletRequest request, ServletResponse response){
this.fillCorsHeader(WebUtils.toHttp(request), WebUtils.toHttp(response));
request.setAttribute("jwtShiroFilter.FILTERED", true);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(this.isLoginRequest(request, response))
return true;
Boolean afterFiltered = (Boolean)(request.getAttribute("jwtShiroFilter.FILTERED"));
if( BooleanUtils.isTrue(afterFiltered))
return true;
boolean allowed = false;
try {
allowed = executeLogin(request, response);
} catch(IllegalStateException e){ //not found any token
e.printStackTrace();
log.error("Not found any token");
}catch (Exception e) {
log.error("Error occurs when login", e);
}
return allowed || super.isPermissive(mappedValue);
}
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
HttpServletRequest request = null;
Optional<String> result = null;
String host = null;
if (servletRequest instanceof HttpServletRequest){
request = (HttpServletRequest)servletRequest;
String token = request.getHeader("authorization");
result = Optional.ofNullable(token)
.filter(item -> item.startsWith("bearer"))
.map(item -> item.substring(6));
host = WebUtil.getRealRemoteAddr(request);
}else {
result = Optional.empty();
}
return new JwtToken(result.orElse(null), host);
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.setStatus(HttpStatus.SC_LENGTH_REQUIRED);
int errorCode = 0;
String errorMsg = "";
try {
if (ResponseCode.TOKEN_EXPIRES.getDesc().equals(servletRequest.getAttribute(this.tokenError))){
errorCode = ResponseCode.TOKEN_EXPIRES.getCode();
errorMsg = ResponseCode.TOKEN_EXPIRES.getDesc();
}else {
errorCode = ResponseCode.ERROR.getCode();
errorMsg = (String)servletRequest.getAttribute(this.tokenError);
}
} catch (Exception e1) {
errorCode = ResponseCode.ERROR.getCode();
errorMsg = ResponseCode.ERROR.getDesc();
e1.printStackTrace();
}
fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse);
ResponseWriter.println(httpResponse, ServerResponse.createByErrorCodeMessage(errorCode, errorMsg));
return false;
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
request.setAttribute(this.tokenError, e.getMessage());
log.error("Validate fail, token:{}, error:{}", token.toString(), e.getMessage());
return false;
}
protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse){
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
}
}
JwtCredentialsMatcher:
@NoArgsConstructor
public class JwtCredentialsMatcher implements CredentialsMatcher {
private JwtUtil jwtUtil;
public JwtCredentialsMatcher(JwtUtil jwtUtil){
this.jwtUtil = jwtUtil;
}
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
JwtToken jwtToken = (JwtToken) authenticationToken;
String token = jwtToken.getToken();
// 校验
try{
jwtUtil.verifyToken(token);
}catch (Exception e){
return false;
}
return true;
}
}
RedisCacheManager:
public class RedisCacheManager implements CacheManager {
private Cache cache;
public void setCache(Cache cache) {
this.cache = cache;
}
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return cache;
}
}
RedisCache:
@Data
@NoArgsConstructor
@Slf4j
public class RedisCache<K,V> implements Cache<K,V> {
private RedisTemplate redisTemplate;
// 过期时间,单位为分钟
private static final Integer expire_time = 30;
// 缓存前缀
private static final String cache_prefix = "shiro-cache:";
public RedisCache(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
}
private String getKey(K k){
if (k instanceof String){
return cache_prefix + k;
}else if(k instanceof SimplePrincipalCollection){
return cache_prefix + ((User)((SimplePrincipalCollection)k).getPrimaryPrincipal()).getUsername();
}
return cache_prefix + k.toString();
}
@Override
public V get(K key) throws CacheException {
V v = (V)redisTemplate.opsForValue().get(getKey(key));
log.info("从redis中取出权限对象[{}]" , v);
return v;
}
@Override
public V put(K k, V v) throws CacheException {
redisTemplate.opsForValue().set(getKey(k), v, expire_time, TimeUnit.SECONDS);
log.info("[k={},v={}]将权限存放到Redis中", k, v);
return v;
}
@Override
public V remove(K k) throws CacheException {
return null;
}
@Override
public void clear() throws CacheException {
}
@Override
public int size() {
return 0;
}
@Override
public Set<K> keys() {
return null;
}
@Override
public Collection<V> values() {
return null;
}
}