前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >shiro改造jwtToken模式中的坎坷二三事

shiro改造jwtToken模式中的坎坷二三事

作者头像
山行AI
发布2019-12-02 22:22:54
2.3K0
发布2019-12-02 22:22:54
举报
文章被收录于专栏:山行AI山行AI

shiro 改造成 jwt token 认证后(如果自定义了 shiroFilter 并且在 onAccessAllow 中加上了 executeLogin 的逻辑可能会避过这个坑)因为 session 被禁用的缘故,每次请求进来后的 subject 中是没有用户信息和权限信息的,所以在做除了登录之外的操作时,后台接口加了注解时会报无权限和未授权的问题。

Subject 的前世今生

org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal:

代码语言:javascript
复制
 protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
 throws ServletException, IOException {
 Throwable t = null;
 try {
 final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
 final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
 final Subject subject = createSubject(request, response);
 //noinspection unchecked
            subject.execute(new Callable() {
 public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
 return null;
 }
 });
 .............

这里会根据 request 和 response 来创建 subject 对象,也就是说如果开启了 session,那么这里创建 subject 时会先从 session 中将用户的权限信息放入到 subject 中去,也就是此时的 subject 是有权限信息的(session 未过期的前提下)。

org.apache.shiro.subject.support.DelegatingSubject#execute(java.util.concurrent.Callable):

代码语言:javascript
复制
public <V> V execute(Callable<V> callable) throws ExecutionException {
 Callable<V> associated = associateWith(callable);
 try {
 return associated.call();
 } catch (Throwable t) {
 throw new ExecutionException(t);
 }
 }

org.apache.shiro.subject.support.SubjectCallable#call:

代码语言:javascript
复制
 public V call() throws Exception {
 try {
            threadState.bind();
 return doCall(this.callable);
 } finally {
            threadState.restore();
 }
 }

org.apache.shiro.subject.support.SubjectThreadState#bind:

代码语言:javascript
复制
 public void bind() {
 SecurityManager securityManager = this.securityManager;
 if ( securityManager == null ) {
 //try just in case the constructor didn't find one at the time:
            securityManager = ThreadContext.getSecurityManager();
 }
 this.originalResources = ThreadContext.getResources();
 ThreadContext.remove();
 ThreadContext.bind(this.subject);
 if (securityManager != null) {
 ThreadContext.bind(securityManager);
 }
 }

org.apache.shiro.util.ThreadContext#bind(org.apache.shiro.subject.Subject):

代码语言:javascript
复制
public static void bind(Subject subject) {
 if (subject != null) {
            put(SUBJECT_KEY, subject);
 }
 }

org.apache.shiro.util.ThreadContext#put:

代码语言:javascript
复制
public static void put(Object key, Object value) {
 if (key == null) {
 throw new IllegalArgumentException("key cannot be null");
 }
 if (value == null) {
            remove(key);
 return;
 }
        ensureResourcesInitialized();
        resources.get().put(key, value);
 if (log.isTraceEnabled()) {
 String msg = "Bound value of type [" + value.getClass().getName() + "] for key [" +
                    key + "] to thread " + "[" + Thread.currentThread().getName() + "]";
            log.trace(msg);
 }
 }

到这里就将当前线程的 ThreadLocal 中放入了 Subject 对象,并建立绑定关系。key 为 org.apache.shiro.util.ThreadContextSUBJECTKEY,value 为 WebDelegatingSubject 对象。

Subject subject = SecurityUtils.getSubject();

org.apache.shiro.SecurityUtils#getSubject:

代码语言:javascript
复制
public static Subject getSubject() {
 Subject subject = ThreadContext.getSubject();
 if (subject == null) {
            subject = (new Subject.Builder()).buildSubject();
 ThreadContext.bind(subject);
 }
 return subject;
 }

org.apache.shiro.util.ThreadContext#getSubject:

代码语言:javascript
复制
 public static Subject getSubject() {
 return (Subject) get(SUBJECT_KEY);
 }

org.apache.shiro.util.ThreadContext#get:

代码语言:javascript
复制
public static Object get(Object key) {
 if (log.isTraceEnabled()) {
 String msg = "get() - in thread [" + Thread.currentThread().getName() + "]";
            log.trace(msg);
 }
 Object value = getValue(key);
 if ((value != null) && log.isTraceEnabled()) {
 String msg = "Retrieved value of type [" + value.getClass().getName() + "] for key [" +
                    key + "] " + "bound to thread [" + Thread.currentThread().getName() + "]";
            log.trace(msg);
 }
 return value;
 }
 private static Object getValue(Object key) {
 Map<Object, Object> perThreadResources = resources.get();
 return perThreadResources != null ? perThreadResources.get(key) : null;
 }
 private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();

获取到与当前线程绑定的 subject 对象。

subject.login(token):

org.apache.shiro.subject.support.DelegatingSubject#login:

代码语言:javascript
复制
public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
 Subject subject = securityManager.login(this, token);
 PrincipalCollection principals;
 String host = null;
 if (subject instanceof DelegatingSubject) {
 DelegatingSubject delegating = (DelegatingSubject) subject;
 //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
            principals = delegating.principals;
            host = delegating.host;
 } else {
            principals = subject.getPrincipals();
 }
 if (principals == null || principals.isEmpty()) {
 String msg = "Principals returned from securityManager.login( token ) returned a null or " +
 "empty value.  This value must be non null and populated with one or more elements.";
 throw new IllegalStateException(msg);
 }
 this.principals = principals;
 this.authenticated = true;
 if (token instanceof HostAuthenticationToken) {
            host = ((HostAuthenticationToken) token).getHost();
 }
 if (host != null) {
 this.host = host;
 }
 Session session = subject.getSession(false);
 if (session != null) {
 this.session = decorate(session);
 } else {
 this.session = null;
 }
 }

org.apache.shiro.mgt.DefaultSecurityManager#login:

代码语言:javascript
复制
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
 AuthenticationInfo info;
 try {
            info = authenticate(token);
 } catch (AuthenticationException ae) {
 try {
                onFailedLogin(token, ae, subject);
 } catch (Exception e) {
 if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an " +
 "exception.  Logging and propagating original AuthenticationException.", e);
 }
 }
 throw ae; //propagate
 }
 Subject loggedIn = createSubject(token, info, subject);
        onSuccessfulLogin(token, info, loggedIn);
 return loggedIn;
 }
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
 SubjectContext context = createSubjectContext();
        context.setAuthenticated(true);
        context.setAuthenticationToken(token);
        context.setAuthenticationInfo(info);
 if (existing != null) {
            context.setSubject(existing);
 }
 return createSubject(context);
 }
public Subject createSubject(SubjectContext subjectContext) {
 //create a copy so we don't modify the argument's backing map:
 SubjectContext context = copy(subjectContext);
 //ensure that the context has a SecurityManager instance, and if not, add one:
        context = ensureSecurityManager(context);
 //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
 //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
 //process is often environment specific - better to shield the SF from these details:
        context = resolveSession(context);
 //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
 //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);
 Subject subject = doCreateSubject(context);
 //save this subject for future reference if necessary:
 //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
 //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
 //Added in 1.2:
        save(subject);
 return subject;
 }
 protected Subject doCreateSubject(SubjectContext context) {
 return getSubjectFactory().createSubject(context);
 }
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
 @Override
 public Subject createSubject(SubjectContext context) {
 //不创建session
        context.setSessionCreationEnabled(false);
 return super.createSubject(context);
 }
}

可见,这里是对线程绑定的 subject 进行包装后创建了一个新的 subject 对象,并通过 save(subject)进行处理(将 subject 放入 session 中),具体的处理逻辑为: org.apache.shiro.mgt.DefaultSubjectDAO#save:

代码语言:javascript
复制
public Subject save(Subject subject) {
 if (isSessionStorageEnabled(subject)) {
            saveToSession(subject);
 } else {
            log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +
 "authentication state are expected to be initialized on every request or invocation.", subject);
 }
 return subject;
 }

我们是禁用 session 的,所以这里的 isSessionStorageEnabled 为 false,也就是说 save 方法没有操作。

这里稍微提下 saveToSession 的操作:

代码语言:javascript
复制
  protected void saveToSession(Subject subject) {
 //performs merge logic, only updating the Subject's session if it does not match the current state:
        mergePrincipals(subject);
        mergeAuthenticationState(subject);
 }
 protected void mergePrincipals(Subject subject) {
 //merge PrincipalCollection state:
 PrincipalCollection currentPrincipals = null;
 //SHIRO-380: added if/else block - need to retain original (source) principals
 //This technique (reflection) is only temporary - a proper long term solution needs to be found,
 //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible
 //
 //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +
 if (subject.isRunAs() && subject instanceof DelegatingSubject) {
 try {
 Field field = DelegatingSubject.class.getDeclaredField("principals");
                field.setAccessible(true);
                currentPrincipals = (PrincipalCollection)field.get(subject);
 } catch (Exception e) {
 throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
 }
 }
 if (currentPrincipals == null || currentPrincipals.isEmpty()) {
            currentPrincipals = subject.getPrincipals();
 }
 Session session = subject.getSession(false);
 if (session == null) {
 if (!isEmpty(currentPrincipals)) {
                session = subject.getSession();
                session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
 }
 // otherwise no session and no principals - nothing to save
 } else {
 PrincipalCollection existingPrincipals =
 (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
 if (isEmpty(currentPrincipals)) {
 if (!isEmpty(existingPrincipals)) {
                    session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
 }
 // otherwise both are null or empty - no need to update the session
 } else {
 if (!currentPrincipals.equals(existingPrincipals)) {
                    session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
 }
 // otherwise they're the same - no need to update the session
 }
 }
 }
 protected void mergeAuthenticationState(Subject subject) {
 Session session = subject.getSession(false);
 if (session == null) {
 if (subject.isAuthenticated()) {
                session = subject.getSession();
                session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
 }
 //otherwise no session and not authenticated - nothing to save
 } else {
 Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
 if (subject.isAuthenticated()) {
 if (existingAuthc == null || !existingAuthc) {
                    session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
 }
 //otherwise authc state matches - no need to update the session
 } else {
 if (existingAuthc != null) {
 //existing doesn't match the current state - remove it:
                    session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
 }
 //otherwise not in the session and not authenticated - no need to update the session
 }
 }
 }

这里的操作无非就是往 session 里塞入键值对,键值对中的 key 为:

代码语言:javascript
复制
 private static final String SECURITY_MANAGER = DefaultSubjectContext.class.getName() + ".SECURITY_MANAGER";
 private static final String SESSION_ID = DefaultSubjectContext.class.getName() + ".SESSION_ID";
 private static final String AUTHENTICATION_TOKEN = DefaultSubjectContext.class.getName() + ".AUTHENTICATION_TOKEN";
 private static final String AUTHENTICATION_INFO = DefaultSubjectContext.class.getName() + ".AUTHENTICATION_INFO";
 private static final String SUBJECT = DefaultSubjectContext.class.getName() + ".SUBJECT";
 private static final String PRINCIPALS = DefaultSubjectContext.class.getName() + ".PRINCIPALS";
 private static final String SESSION = DefaultSubjectContext.class.getName() + ".SESSION";
 private static final String AUTHENTICATED = DefaultSubjectContext.class.getName() + ".AUTHENTICATED";
 private static final String HOST = DefaultSubjectContext.class.getName() + ".HOST";
 public static final String SESSION_CREATION_ENABLED = DefaultSubjectContext.class.getName() + ".SESSION_CREATION_ENABLED";
 /**
     * The session key that is used to store subject principals.
     */
 public static final String PRINCIPALS_SESSION_KEY = DefaultSubjectContext.class.getName() + "_PRINCIPALS_SESSION_KEY";
 /**
     * The session key that is used to store whether or not the user is authenticated.
     */
 public static final String AUTHENTICATED_SESSION_KEY = DefaultSubjectContext.class.getName() + "_AUTHENTICATED_SESSION_KEY";

禁用了 session 后,subject 是没有存放在 session 中的,所以当调用 subject.login 方法后的这个 subject 是有权限信息的,其他请求进入时创建的 subject 则不然。

这时我们再回头看一眼 login 方法里调用的 authenticate 方法 org.apache.shiro.mgt.AuthenticatingSecurityManager#authenticate:

代码语言:javascript
复制
   public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
 return this.authenticator.authenticate(token);
 }

调用的是 org.apache.shiro.authc.AbstractAuthenticator#authenticate:

代码语言:javascript
复制
 public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
 if (token == null) {
 throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
 }
        log.trace("Authentication attempt received for token [{}]", token);
 AuthenticationInfo info;
 try {
            info = doAuthenticate(token);
 if (info == null) {
 String msg = "No account information found for authentication token [" + token + "] by this " +
 "Authenticator instance.  Please check that it is configured correctly.";
 throw new AuthenticationException(msg);
 }
 } catch (Throwable t) {
 AuthenticationException ae = null;
 if (t instanceof AuthenticationException) {
                ae = (AuthenticationException) t;
 }
 if (ae == null) {
 //Exception thrown was not an expected AuthenticationException.  Therefore it is probably a little more
 //severe or unexpected.  So, wrap in an AuthenticationException, log to warn, and propagate:
 String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected " +
 "error? (Typical or expected login exceptions should extend from AuthenticationException).";
                ae = new AuthenticationException(msg, t);
 if (log.isWarnEnabled())
                    log.warn(msg, t);
 }
 try {
                notifyFailure(token, ae);
 } catch (Throwable t2) {
 if (log.isWarnEnabled()) {
 String msg = "Unable to send notification for failed authentication attempt - listener error?.  " +
 "Please check your AuthenticationListener implementation(s).  Logging sending exception " +
 "and propagating original AuthenticationException instead...";
                    log.warn(msg, t2);
 }
 }
 throw ae;
 }
        log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);
        notifySuccess(token, info);
 return info;
 }

这里我们继续看 doAuthenticate 即 org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate:

代码语言:javascript
复制
   protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
 Collection<Realm> realms = getRealms();
 if (realms.size() == 1) {
 return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
 } else {
 return doMultiRealmAuthentication(realms, authenticationToken);
 }
 }
 protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
 if (!realm.supports(token)) {
 String msg = "Realm [" + realm + "] does not support authentication token [" +
                    token + "].  Please ensure that the appropriate Realm implementation is " +
 "configured correctly or that the realm accepts AuthenticationTokens of this type.";
 throw new UnsupportedTokenException(msg);
 }
 AuthenticationInfo info = realm.getAuthenticationInfo(token);
 if (info == null) {
 String msg = "Realm [" + realm + "] was unable to find account data for the " +
 "submitted AuthenticationToken [" + token + "].";
 throw new UnknownAccountException(msg);
 }
 return info;
 }

org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo:

代码语言:javascript
复制
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
 AuthenticationInfo info = getCachedAuthenticationInfo(token);
 if (info == null) {
 //otherwise not cached, perform the lookup:
            info = doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
 if (token != null && info != null) {
                cacheAuthenticationInfoIfPossible(token, info);
 }
 } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
 }
 if (info != null) {
            assertCredentialsMatch(token, info);
 } else {
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
 }
 return info;
 }

doGetAuthenticationInfo 调用的是自定义 realm 的 doGetAuthenticationInfo 实现:

代码语言:javascript
复制
 /**
     *   认证
     */
 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
 StatelessToken statelessToken = (StatelessToken) token;
 String username = (String) statelessToken.getPrincipal();
 //密码,shiro会根据token的credentials进行加密然后与SimpleAuthenticationInfo的第二个参数进行比较
 //String loginedToken = (String) statelessToken.getCredentials();
 //通过校验
 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                username,
                statelessToken.getToken(),
 ByteSource.Util.bytes(statelessToken.getSalt()),
                getName()
 );
 return authenticationInfo;
 }

我们再合起来看下,登录时的代码简化如下:

代码语言:javascript
复制
  AuthenticationInfo info = authenticate(token);
 Subject loggedIn = createSubject(token, info, subject);

这时生成的 subject 是根据 authenticate 即认证后得到的 AuthenticationInfo 信息来的,也就是说这些认证的信息是保存在 subject 中的,也就是下文中要提到的 subject 的 principals 和 authenticated 属性。

注解的校验

注解的启用方式

代码语言:javascript
复制
@Bean
 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
 AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
 return authorizationAttributeSourceAdvisor;
 }
 public AuthorizationAttributeSourceAdvisor() {
        setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
 }

关于 AuthorizationAttributeSourceAdvisor:

代码语言:javascript
复制
public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {
 private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class);
 private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
 new Class[] {
 RequiresPermissions.class, RequiresRoles.class,
 RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
 };
 public boolean matches(Method method, Class targetClass) {
 Method m = method;
 if ( isAuthzAnnotationPresent(m) ) {
 return true;
 }
 //The 'method' parameter could be from an interface that doesn't have the annotation.
 //Check to see if the implementation has it.
 if ( targetClass != null) {
 try {
                m = targetClass.getMethod(m.getName(), m.getParameterTypes());
 return isAuthzAnnotationPresent(m) || isAuthzAnnotationPresent(targetClass);
 } catch (NoSuchMethodException ignored) {
 //default return value is false.  If we can't find the method, then obviously
 //there is no annotation, so just use the default return value.
 }
 }
 return false;
 }
 private boolean isAuthzAnnotationPresent(Class<?> targetClazz) {
 for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
 Annotation a = AnnotationUtils.findAnnotation(targetClazz, annClass);
 if ( a != null ) {
 return true;
 }
 }
 return false;
 }
 private boolean isAuthzAnnotationPresent(Method method) {
 for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
 Annotation a = AnnotationUtils.findAnnotation(method, annClass);
 if ( a != null ) {
 return true;
 }
 }
 return false;
 }

这里简要的说以下几点:

  • 继承自 StaticMethodMatcherPointcutAdvisor,可以对方法进行 aop 切面编程。
  • matches 方法会对方法上加有 AUTHZANNOTATIONCLASSES 列表中的注解的方法进行切面。

关于 AopAllianceAnnotationsAuthorizingMethodInterceptor:

代码语言:javascript
复制
public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
 List<AuthorizingAnnotationMethodInterceptor> interceptors =
 new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);
 //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
 //raw JDK resolution process.
 AnnotationResolver resolver = new SpringAnnotationResolver();
 //we can re-use the same resolver instance - it does not retain state:
        interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
        interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
        interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
        interceptors.add(new UserAnnotationMethodInterceptor(resolver));
        interceptors.add(new GuestAnnotationMethodInterceptor(resolver));
        setMethodInterceptors(interceptors);
 }

根据构造方法可以看出,是加了很多的拦截器。

我们以 AuthenticatedAnnotationMethodInterceptor 为例:

代码语言:javascript
复制
  public AuthenticatedAnnotationMethodInterceptor(AnnotationResolver resolver) {
 super(new AuthenticatedAnnotationHandler(), resolver);
 }

AuthenticatedAnnotationHandler:

代码语言:javascript
复制
 public AuthenticatedAnnotationHandler() {
 super(RequiresAuthentication.class);
 }
 public void assertAuthorized(Annotation a) throws UnauthenticatedException {
 if (a instanceof RequiresAuthentication && !getSubject().isAuthenticated() ) {
 throw new UnauthenticatedException( "The current Subject is not authenticated.  Access denied." );
 }
 }

是通过 getSubject().isAuthenticated()来做校验的,也就是 subject 的 protected boolean authenticated 属性。

我们再看下 PermissionAnnotationMethodInterceptor:

代码语言:javascript
复制
public PermissionAnnotationMethodInterceptor() {
 super( new PermissionAnnotationHandler() );
 }
 public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) {
 super( new PermissionAnnotationHandler(), resolver);
 }

PermissionAnnotationHandler:

代码语言:javascript
复制
 protected String[] getAnnotationValue(Annotation a) {
 RequiresPermissions rpAnnotation = (RequiresPermissions) a;
 return rpAnnotation.value();
 }
 public void assertAuthorized(Annotation a) throws AuthorizationException {
 if (!(a instanceof RequiresPermissions)) return;
 RequiresPermissions rpAnnotation = (RequiresPermissions) a;
 String[] perms = getAnnotationValue(a);
 Subject subject = getSubject();
 if (perms.length == 1) {
            subject.checkPermission(perms[0]);
 return;
 }
 if (Logical.AND.equals(rpAnnotation.logical())) {
            getSubject().checkPermissions(perms);
 return;
 }
 if (Logical.OR.equals(rpAnnotation.logical())) {
 // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
 boolean hasAtLeastOnePermission = false;
 for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true;
 // Cause the exception if none of the role match, note that the exception message will be a bit misleading
 if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]);
 }
 }

org.apache.shiro.subject.support.DelegatingSubject#checkPermission(java.lang.String):

代码语言:javascript
复制
public void checkPermission(String permission) throws AuthorizationException {
        assertAuthzCheckPossible();
        securityManager.checkPermission(getPrincipals(), permission);
 }

org.apache.shiro.mgt.AuthorizingSecurityManager#checkPermission(org.apache.shiro.subject.PrincipalCollection, java.lang.String):

代码语言:javascript
复制
public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException {
 this.authorizer.checkPermission(principals, permission);
 }

org.apache.shiro.realm.AuthorizingRealm#checkPermission(org.apache.shiro.subject.PrincipalCollection, java.lang.String):

代码语言:javascript
复制
public void checkPermission(PrincipalCollection subjectIdentifier, String permission) throws AuthorizationException {
 Permission p = getPermissionResolver().resolvePermission(permission);
        checkPermission(subjectIdentifier, p);
 }
 public void checkPermission(PrincipalCollection principal, Permission permission) throws AuthorizationException {
 AuthorizationInfo info = getAuthorizationInfo(principal);
        checkPermission(permission, info);
 }
 org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo:
 protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
 if (principals == null) {
 return null;
 }
 AuthorizationInfo info = null;
 if (log.isTraceEnabled()) {
            log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
 }
 Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
 if (cache != null) {
 if (log.isTraceEnabled()) {
                log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
 }
 Object key = getAuthorizationCacheKey(principals);
            info = cache.get(key);
 if (log.isTraceEnabled()) {
 if (info == null) {
                    log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
 } else {
                    log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
 }
 }
 }
 if (info == null) {
 // Call template method if the info was not found in a cache
            info = doGetAuthorizationInfo(principals);
 // If the info is not null and the cache has been created, then cache the authorization info.
 if (info != null && cache != null) {
 if (log.isTraceEnabled()) {
                    log.trace("Caching authorization info for principals: [" + principals + "].");
 }
 Object key = getAuthorizationCacheKey(principals);
                cache.put(key, info);
 }
 }
 return info;
 }

接下来就到了很熟悉的 doGetAuthorizationInfo 方法了,它是一个抽象方法,由用户自定义的 realm 来实现,我的实现为:

代码语言:javascript
复制
com.shimh.oauth.OAuthRealm#doGetAuthorizationInfo:
 //授权
 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
 String account = (String) principals.getPrimaryPrincipal();
 User user = userService.getUserByAccount(account);
 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
 Set<String> roles = new HashSet<String>();
 //简单处理   只有admin一个角色
 if (user.getAdmin()) {
            roles.add(BaseConstant.ROLE_ADMIN);
 }
        authorizationInfo.setRoles(roles);
 return authorizationInfo;
 }

这里需要调用 subject 的 principals 来获取用户信息。

RoleAnnotationMethodInterceptor:

代码语言:javascript
复制
public RoleAnnotationMethodInterceptor(AnnotationResolver resolver) {
 super(new RoleAnnotationHandler(), resolver);
 }

org.apache.shiro.authz.aop.RoleAnnotationHandler#RoleAnnotationHandler:

代码语言:javascript
复制
 public RoleAnnotationHandler() {
 super(RequiresRoles.class);
 }
 public void assertAuthorized(Annotation a) throws AuthorizationException {
 if (!(a instanceof RequiresRoles)) return;
 RequiresRoles rrAnnotation = (RequiresRoles) a;
 String[] roles = rrAnnotation.value();
 if (roles.length == 1) {
            getSubject().checkRole(roles[0]);
 return;
 }
 if (Logical.AND.equals(rrAnnotation.logical())) {
            getSubject().checkRoles(Arrays.asList(roles));
 return;
 }
 if (Logical.OR.equals(rrAnnotation.logical())) {
 // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
 boolean hasAtLeastOneRole = false;
 for (String role : roles) if (getSubject().hasRole(role)) hasAtLeastOneRole = true;
 // Cause the exception if none of the role match, note that the exception message will be a bit misleading
 if (!hasAtLeastOneRole) getSubject().checkRole(roles[0]);
 }
 }

org.apache.shiro.subject.support.DelegatingSubject#checkRole:

代码语言:javascript
复制
public void checkRole(String role) throws AuthorizationException {
        assertAuthzCheckPossible();
        securityManager.checkRole(getPrincipals(), role);
 }

org.apache.shiro.mgt.AuthorizingSecurityManager#checkRole:

代码语言:javascript
复制
public void checkRole(PrincipalCollection principals, String role) throws AuthorizationException {
 this.authorizer.checkRole(principals, role);
 }

org.apache.shiro.realm.AuthorizingRealm#checkRole(org.apache.shiro.subject.PrincipalCollection, java.lang.String):

代码语言:javascript
复制
 public void checkRole(PrincipalCollection principal, String role) throws AuthorizationException {
 AuthorizationInfo info = getAuthorizationInfo(principal);
        checkRole(role, info);
 }

接下来调用的方法和上面的 PermissionAnnotationMethodInterceptor 相同,这里就不再赘述。最后也还是会调用自定义 Realm 的授权方法 doGetAuthorizationInfo。

改成无状态后,非登录请求的表现

ArticleController 的发布方法如下:

代码语言:javascript
复制
@PostMapping("/publish")
 @RequiresAuthentication
 @LogAnnotation(module = "文章", operation = "发布文章")
 @AuthPassport(noNeed = true)
 public Result saveArticle(@Validated @RequestBody Article article, @CurrentUser UserTo userTo) {
 Integer articleId = articleService.publishArticle(article,userTo);
 Result r = Result.success();
        r.simple().put("articleId", articleId);
 return r;
 }

可见是加了 RequiresAuthentication 注解的,请求时会被拦截如下: org.apache.shiro.spring.security.interceptor.AopAllianceAnnotationsAuthorizingMethodInterceptor#invoke:

代码语言:javascript
复制
 public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation);
 return super.invoke(mi);
 }

然后调用 org.apache.shiro.authz.aop.AuthorizingMethodInterceptor#invoke:

代码语言:javascript
复制
 public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        assertAuthorized(methodInvocation);
 return methodInvocation.proceed();
 }

org.apache.shiro.authz.aop.AnnotationsAuthorizingMethodInterceptor#assertAuthorized:

代码语言:javascript
复制
protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException {
 //default implementation just ensures no deny votes are cast:
 Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();
 if (aamis != null && !aamis.isEmpty()) {
 for (AuthorizingAnnotationMethodInterceptor aami : aamis) {
 if (aami.supports(methodInvocation)) {
                    aami.assertAuthorized(methodInvocation);
 }
 }
 }
 }

org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor#assertAuthorized:

代码语言:javascript
复制
public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
 try {
 ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi));
 }
 catch(AuthorizationException ae) {
 // Annotation handler doesn't know why it was called, so add the information here if possible.
 // Don't wrap the exception here since we don't want to mask the specific exception, such as
 // UnauthenticatedException etc.
 if (ae.getCause() == null) ae.initCause(new AuthorizationException("Not authorized to invoke method: " + mi.getMethod()));
 throw ae;
 }
 }

org.apache.shiro.authz.aop.AuthenticatedAnnotationHandler#assertAuthorized:

代码语言:javascript
复制
public void assertAuthorized(Annotation a) throws UnauthenticatedException {
 if (a instanceof RequiresAuthentication && !getSubject().isAuthenticated() ) {
 throw new UnauthenticatedException( "The current Subject is not authenticated.  Access denied." );
 }
 }

其中 org.apache.shiro.aop.AnnotationHandler#getSubject:

代码语言:javascript
复制
protected Subject getSubject() {
 return SecurityUtils.getSubject();
 }

关于这个方法的流程上面已经讲过,其实就是把与当前 IO 线程绑定的 subject 对象取出来的过程,如果开启了 session 会同步 session 中的信息(本文开头部分有讲过),如果没有开启 session,里面是没有任何权限和用户信息的。

这里补充一下从 session 中同步信息到 subject 中的流程: org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal:

代码语言:javascript
复制
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
 throws ServletException, IOException {
 Throwable t = null;
 try {
 final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
 final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
 final Subject subject = createSubject(request, response);
 //noinspection unchecked
            subject.execute(new Callable() {
 public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
 return null;
 }
 });
 } catch (ExecutionException ex) {
            t = ex.getCause();
 } catch (Throwable throwable) {
            t = throwable;
 }
 if (t != null) {
 if (t instanceof ServletException) {
 throw (ServletException) t;
 }
 if (t instanceof IOException) {
 throw (IOException) t;
 }
 //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
 String msg = "Filtered request failed.";
 throw new ServletException(msg, t);
 }
 }
 protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
 return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
 }

org.apache.shiro.web.subject.WebSubject.Builder#buildWebSubject:

代码语言:javascript
复制
public WebSubject buildWebSubject() {
 Subject subject = super.buildSubject();
 if (!(subject instanceof WebSubject)) {
 String msg = "Subject implementation returned from the SecurityManager was not a " +
 WebSubject.class.getName() + " implementation.  Please ensure a Web-enabled SecurityManager " +
 "has been configured and made available to this builder.";
 throw new IllegalStateException(msg);
 }
 return (WebSubject) subject;
 }

org.apache.shiro.subject.Subject.Builder#buildSubject:

代码语言:javascript
复制
public Subject buildSubject() {
 return this.securityManager.createSubject(this.subjectContext);
 }

org.apache.shiro.mgt.DefaultSecurityManager#createSubject(org.apache.shiro.subject.SubjectContext):

代码语言:javascript
复制
public Subject createSubject(SubjectContext subjectContext) {
 //create a copy so we don't modify the argument's backing map:
 SubjectContext context = copy(subjectContext);
 //ensure that the context has a SecurityManager instance, and if not, add one:
        context = ensureSecurityManager(context);
 //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
 //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
 //process is often environment specific - better to shield the SF from these details:
        context = resolveSession(context);
 //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
 //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);
 Subject subject = doCreateSubject(context);
 //save this subject for future reference if necessary:
 //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
 //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
 //Added in 1.2:
        save(subject);
 return subject;
 }

也就是说如果关闭了 session,就需要自己想办法往这个 subject 中添加用户的权限信息。

方案

方案一

加一个 filter 如下:

代码语言:javascript
复制
public class JwtAuthFilter extends AuthenticatingFilter {
 /**
     * 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。
     */
 @Override
 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
 if(this.isLoginRequest(request, response))
 return true;
 boolean allowed = false;
 try {
            allowed = executeLogin(request, response);
 } catch(IllegalStateException e){ //not found any token
            log.error("Not found any token");
 }catch (Exception e) {
            log.error("Error occurs when login", e);
 }
 return allowed || super.isPermissive(mappedValue);
 }
 /**
     * 这里重写了父类的方法,使用我们自己定义的Token类,提交给shiro。这个方法返回null的话会直接抛出异常,进入isAccessAllowed()的异常处理逻辑。
     */
 @Override
 protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
 //这里是存放在请求的header中的,也可以存放在cookie中
 String jwtToken = getAuthzHeader(servletRequest);
 if(StringUtils.isNotBlank(jwtToken)&&!JwtUtils.isTokenExpired(jwtToken))
 return new JWTToken(jwtToken);
 return null;
 }
 ..........

executeLogin 中会调用 createToken 和 getSubject 方法,进行 subject.login 操作:

代码语言:javascript
复制
 protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
 AuthenticationToken token = createToken(request, response);
 if (token == null) {
 String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
 "must be created in order to execute a login attempt.";
 throw new IllegalStateException(msg);
 }
 try {
 Subject subject = getSubject(request, response);
            subject.login(token);
 return onLoginSuccess(token, subject, request, response);
 } catch (AuthenticationException e) {
 return onLoginFailure(token, e, request, response);
 }
 }

其中 createToken 为自定义的 filter 中重写的方法,getSubject 方法如下:

代码语言:javascript
复制
protected Subject getSubject(ServletRequest request, ServletResponse response) {
 return SecurityUtils.getSubject();
 }

依然是我们很熟悉的那个 subject。

该方案引用自网络,个人认为理论上是可行的,并未自测,出自:https://www.jianshu.com/p/0b1131be7ace

方案二

自定义拦截器,然后调用 subject.login 来验证:

代码语言:javascript
复制
Cookie[] cookies = request.getCookies();
 if (cookies != null) {
 Optional<Cookie> cookieOptional = Arrays.stream(cookies).filter(cookie -> BaseConstant.AUTH.equals(cookie.getName())).findFirst();
 if (cookieOptional.isPresent()) {
 Cookie cookie = cookieOptional.get();
 try {
 Map<String, Object> paramMap = JwtUtil.decode(BaseConstant.SHIRO_KEY, cookie.getValue(), BaseConstant.SHIRO_SALT);
 String userObj = (String)paramMap.get("user");
 UserTo userTo = JSONObject.parseObject(userObj, UserTo.class);
 Subject subject = SecurityUtils.getSubject();
 //调用了login之后会校验OAuthRealm的supports方法,查看token类型
 //注意,这里调用subject.login是为了给当前线程的subject进行认证操作,赋给principles和authorized属性值
 StatelessToken token = new StatelessToken(userTo.getAccount(),cookie.getValue(),BaseConstant.SHIRO_SALT);
                    subject.login(token);
 return Optional.of(userTo);
 } catch (Exception e) {
                    log.error("cookie的值为:" + cookie.getValue() + "解密失败!", e);
 }
 }
 }
 return Optional.empty();

其中 OAuthRealm 的代码如下:

代码语言:javascript
复制
public class OAuthRealm extends AuthorizingRealm {
 private static final Logger LOG = LoggerFactory.getLogger(OAuthRealm.class);
 @Autowired
 private UserService userService;
 public OAuthRealm() {
 this.setCredentialsMatcher(new JWTCredentialsMatcher());
 }
 @Override
 public boolean supports(AuthenticationToken token) {
 //仅支持StatelessToken类型的
 return token instanceof StatelessToken;
 }
 //授权
 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
 String account = (String) principals.getPrimaryPrincipal();
 User user = userService.getUserByAccount(account);
 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
 Set<String> roles = new HashSet<String>();
 //简单处理   只有admin一个角色
 if (user.getAdmin()) {
            roles.add(BaseConstant.ROLE_ADMIN);
 }
        authorizationInfo.setRoles(roles);
 return authorizationInfo;
 }
 /**
     *   认证
     */
 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
 StatelessToken statelessToken = (StatelessToken) token;
 String username = (String) statelessToken.getPrincipal();
 //密码,shiro会根据token的credentials进行加密然后与SimpleAuthenticationInfo的第二个参数进行比较
 //String loginedToken = (String) statelessToken.getCredentials();
 //通过校验
 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                username,
                statelessToken.getCredentials(),
 ByteSource.Util.bytes(statelessToken.getSalt()),
                getName()
 );
 return authenticationInfo;
 }
}

JWTCredentialsMatcher 代码如下:

代码语言:javascript
复制
public class JWTCredentialsMatcher implements CredentialsMatcher{
 /**
     * 这里只是简单实现下,可以使用jwt的verify方法来校验
     * @param token
     * @param info
     * @return
     */
 @Override
 public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
 if (info instanceof SimpleAuthenticationInfo){
 SimpleAuthenticationInfo simpleAuthenticationInfo = (SimpleAuthenticationInfo) info;
 PrincipalCollection principals = info.getPrincipals();
 if (principals.isEmpty()){
 return false;
 }
 if (token instanceof StatelessToken){
 String primaryPrincipal = (String) principals.getPrimaryPrincipal();
 String tokenPrincipal = (String) token.getPrincipal();
 String credentials = (String) info.getCredentials();
 String credentials1 = (String) token.getCredentials();
 String toHex = simpleAuthenticationInfo.getCredentialsSalt().toHex();
 StatelessToken statelessToken = (StatelessToken) token;
 String toHex1 = ByteSource.Util.bytes(statelessToken.getSalt()).toHex();
 if (primaryPrincipal.equals(tokenPrincipal) && credentials.equals(credentials1) && toHex.equals(toHex1)){
 return true;
 }
 return false;
 }else {
 return false;
 }
 }
 return false;
 }
}

然后就可以成功了,亲测有效。可以结合注解注入当前用户(之前的文章详细地讲解过,这里不再赘述)

亲测有效哦

参考

  • https://www.jianshu.com/p/0b1131be7ace
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开发架构二三事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Subject 的前世今生
  • 注解的校验
    • 注解的启用方式
      • 我们以 AuthenticatedAnnotationMethodInterceptor 为例:
        • 我们再看下 PermissionAnnotationMethodInterceptor:
          • RoleAnnotationMethodInterceptor:
          • 改成无状态后,非登录请求的表现
          • 方案
            • 方案一
              • 方案二
              • 参考
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档