暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

开发架构二三事 2019-11-29
441

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

Subject 的前世今生

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

  1. protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)

  2. throws ServletException, IOException {

  3. Throwable t = null;

  4. try {

  5. final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);

  6. final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

  7. final Subject subject = createSubject(request, response);

  8. //noinspection unchecked

  9. subject.execute(new Callable() {

  10. public Object call() throws Exception {

  11. updateSessionLastAccessTime(request, response);

  12. executeChain(request, response, chain);

  13. return null;

  14. }

  15. });

  16. .............

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

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

  1. public <V> V execute(Callable<V> callable) throws ExecutionException {

  2. Callable<V> associated = associateWith(callable);

  3. try {

  4. return associated.call();

  5. } catch (Throwable t) {

  6. throw new ExecutionException(t);

  7. }

  8. }

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

  1. public V call() throws Exception {

  2. try {

  3. threadState.bind();

  4. return doCall(this.callable);

  5. } finally {

  6. threadState.restore();

  7. }

  8. }

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

  1. public void bind() {

  2. SecurityManager securityManager = this.securityManager;

  3. if ( securityManager == null ) {

  4. //try just in case the constructor didn't find one at the time:

  5. securityManager = ThreadContext.getSecurityManager();

  6. }

  7. this.originalResources = ThreadContext.getResources();

  8. ThreadContext.remove();

  9. ThreadContext.bind(this.subject);

  10. if (securityManager != null) {

  11. ThreadContext.bind(securityManager);

  12. }

  13. }

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

  1. public static void bind(Subject subject) {

  2. if (subject != null) {

  3. put(SUBJECT_KEY, subject);

  4. }

  5. }

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

  1. public static void put(Object key, Object value) {

  2. if (key == null) {

  3. throw new IllegalArgumentException("key cannot be null");

  4. }

  5. if (value == null) {

  6. remove(key);

  7. return;

  8. }

  9. ensureResourcesInitialized();

  10. resources.get().put(key, value);

  11. if (log.isTraceEnabled()) {

  12. String msg = "Bound value of type [" + value.getClass().getName() + "] for key [" +

  13. key + "] to thread " + "[" + Thread.currentThread().getName() + "]";

  14. log.trace(msg);

  15. }

  16. }

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

Subject subject = SecurityUtils.getSubject();

org.apache.shiro.SecurityUtils#getSubject:

  1. public static Subject getSubject() {

  2. Subject subject = ThreadContext.getSubject();

  3. if (subject == null) {

  4. subject = (new Subject.Builder()).buildSubject();

  5. ThreadContext.bind(subject);

  6. }

  7. return subject;

  8. }

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

  1. public static Subject getSubject() {

  2. return (Subject) get(SUBJECT_KEY);

  3. }

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

  1. public static Object get(Object key) {

  2. if (log.isTraceEnabled()) {

  3. String msg = "get() - in thread [" + Thread.currentThread().getName() + "]";

  4. log.trace(msg);

  5. }

  6. Object value = getValue(key);

  7. if ((value != null) && log.isTraceEnabled()) {

  8. String msg = "Retrieved value of type [" + value.getClass().getName() + "] for key [" +

  9. key + "] " + "bound to thread [" + Thread.currentThread().getName() + "]";

  10. log.trace(msg);

  11. }

  12. return value;

  13. }

  14. private static Object getValue(Object key) {

  15. Map<Object, Object> perThreadResources = resources.get();

  16. return perThreadResources != null ? perThreadResources.get(key) : null;

  17. }

  18. private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();

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

subject.login(token):

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

  1. public void login(AuthenticationToken token) throws AuthenticationException {

  2. clearRunAsIdentitiesInternal();

  3. Subject subject = securityManager.login(this, token);

  4. PrincipalCollection principals;

  5. String host = null;

  6. if (subject instanceof DelegatingSubject) {

  7. DelegatingSubject delegating = (DelegatingSubject) subject;

  8. //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:

  9. principals = delegating.principals;

  10. host = delegating.host;

  11. } else {

  12. principals = subject.getPrincipals();

  13. }

  14. if (principals == null || principals.isEmpty()) {

  15. String msg = "Principals returned from securityManager.login( token ) returned a null or " +

  16. "empty value. This value must be non null and populated with one or more elements.";

  17. throw new IllegalStateException(msg);

  18. }

  19. this.principals = principals;

  20. this.authenticated = true;

  21. if (token instanceof HostAuthenticationToken) {

  22. host = ((HostAuthenticationToken) token).getHost();

  23. }

  24. if (host != null) {

  25. this.host = host;

  26. }

  27. Session session = subject.getSession(false);

  28. if (session != null) {

  29. this.session = decorate(session);

  30. } else {

  31. this.session = null;

  32. }

  33. }

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

  1. public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {

  2. AuthenticationInfo info;

  3. try {

  4. info = authenticate(token);

  5. } catch (AuthenticationException ae) {

  6. try {

  7. onFailedLogin(token, ae, subject);

  8. } catch (Exception e) {

  9. if (log.isInfoEnabled()) {

  10. log.info("onFailedLogin method threw an " +

  11. "exception. Logging and propagating original AuthenticationException.", e);

  12. }

  13. }

  14. throw ae; //propagate

  15. }

  16. Subject loggedIn = createSubject(token, info, subject);

  17. onSuccessfulLogin(token, info, loggedIn);

  18. return loggedIn;

  19. }

  20. protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {

  21. SubjectContext context = createSubjectContext();

  22. context.setAuthenticated(true);

  23. context.setAuthenticationToken(token);

  24. context.setAuthenticationInfo(info);

  25. if (existing != null) {

  26. context.setSubject(existing);

  27. }

  28. return createSubject(context);

  29. }

  30. public Subject createSubject(SubjectContext subjectContext) {

  31. //create a copy so we don't modify the argument's backing map:

  32. SubjectContext context = copy(subjectContext);

  33. //ensure that the context has a SecurityManager instance, and if not, add one:

  34. context = ensureSecurityManager(context);

  35. //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before

  36. //sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the

  37. //process is often environment specific - better to shield the SF from these details:

  38. context = resolveSession(context);

  39. //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first

  40. //if possible before handing off to the SubjectFactory:

  41. context = resolvePrincipals(context);

  42. Subject subject = doCreateSubject(context);

  43. //save this subject for future reference if necessary:

  44. //(this is needed here in case rememberMe principals were resolved and they need to be stored in the

  45. //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).

  46. //Added in 1.2:

  47. save(subject);

  48. return subject;

  49. }

  50. protected Subject doCreateSubject(SubjectContext context) {

  51. return getSubjectFactory().createSubject(context);

  52. }

  53. public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {

  54. @Override

  55. public Subject createSubject(SubjectContext context) {

  56. //不创建session

  57. context.setSessionCreationEnabled(false);

  58. return super.createSubject(context);

  59. }

  60. }

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

  1. public Subject save(Subject subject) {

  2. if (isSessionStorageEnabled(subject)) {

  3. saveToSession(subject);

  4. } else {

  5. log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +

  6. "authentication state are expected to be initialized on every request or invocation.", subject);

  7. }

  8. return subject;

  9. }

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

这里稍微提下 saveToSession 的操作:

  1. protected void saveToSession(Subject subject) {

  2. //performs merge logic, only updating the Subject's session if it does not match the current state:

  3. mergePrincipals(subject);

  4. mergeAuthenticationState(subject);

  5. }

  6. protected void mergePrincipals(Subject subject) {

  7. //merge PrincipalCollection state:

  8. PrincipalCollection currentPrincipals = null;

  9. //SHIRO-380: added if/else block - need to retain original (source) principals

  10. //This technique (reflection) is only temporary - a proper long term solution needs to be found,

  11. //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible

  12. //

  13. //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +

  14. if (subject.isRunAs() && subject instanceof DelegatingSubject) {

  15. try {

  16. Field field = DelegatingSubject.class.getDeclaredField("principals");

  17. field.setAccessible(true);

  18. currentPrincipals = (PrincipalCollection)field.get(subject);

  19. } catch (Exception e) {

  20. throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);

  21. }

  22. }

  23. if (currentPrincipals == null || currentPrincipals.isEmpty()) {

  24. currentPrincipals = subject.getPrincipals();

  25. }

  26. Session session = subject.getSession(false);

  27. if (session == null) {

  28. if (!isEmpty(currentPrincipals)) {

  29. session = subject.getSession();

  30. session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);

  31. }

  32. // otherwise no session and no principals - nothing to save

  33. } else {

  34. PrincipalCollection existingPrincipals =

  35. (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);

  36. if (isEmpty(currentPrincipals)) {

  37. if (!isEmpty(existingPrincipals)) {

  38. session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);

  39. }

  40. // otherwise both are null or empty - no need to update the session

  41. } else {

  42. if (!currentPrincipals.equals(existingPrincipals)) {

  43. session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);

  44. }

  45. // otherwise they're the same - no need to update the session

  46. }

  47. }

  48. }

  49. protected void mergeAuthenticationState(Subject subject) {

  50. Session session = subject.getSession(false);

  51. if (session == null) {

  52. if (subject.isAuthenticated()) {

  53. session = subject.getSession();

  54. session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);

  55. }

  56. //otherwise no session and not authenticated - nothing to save

  57. } else {

  58. Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);

  59. if (subject.isAuthenticated()) {

  60. if (existingAuthc == null || !existingAuthc) {

  61. session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);

  62. }

  63. //otherwise authc state matches - no need to update the session

  64. } else {

  65. if (existingAuthc != null) {

  66. //existing doesn't match the current state - remove it:

  67. session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);

  68. }

  69. //otherwise not in the session and not authenticated - no need to update the session

  70. }

  71. }

  72. }

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

  1. private static final String SECURITY_MANAGER = DefaultSubjectContext.class.getName() + ".SECURITY_MANAGER";

  2. private static final String SESSION_ID = DefaultSubjectContext.class.getName() + ".SESSION_ID";

  3. private static final String AUTHENTICATION_TOKEN = DefaultSubjectContext.class.getName() + ".AUTHENTICATION_TOKEN";

  4. private static final String AUTHENTICATION_INFO = DefaultSubjectContext.class.getName() + ".AUTHENTICATION_INFO";

  5. private static final String SUBJECT = DefaultSubjectContext.class.getName() + ".SUBJECT";

  6. private static final String PRINCIPALS = DefaultSubjectContext.class.getName() + ".PRINCIPALS";

  7. private static final String SESSION = DefaultSubjectContext.class.getName() + ".SESSION";

  8. private static final String AUTHENTICATED = DefaultSubjectContext.class.getName() + ".AUTHENTICATED";

  9. private static final String HOST = DefaultSubjectContext.class.getName() + ".HOST";

  10. public static final String SESSION_CREATION_ENABLED = DefaultSubjectContext.class.getName() + ".SESSION_CREATION_ENABLED";

  11. /**

  12. * The session key that is used to store subject principals.

  13. */

  14. public static final String PRINCIPALS_SESSION_KEY = DefaultSubjectContext.class.getName() + "_PRINCIPALS_SESSION_KEY";

  15. /**

  16. * The session key that is used to store whether or not the user is authenticated.

  17. */

  18. 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:

  1. public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {

  2. return this.authenticator.authenticate(token);

  3. }

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

  1. public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {

  2. if (token == null) {

  3. throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");

  4. }

  5. log.trace("Authentication attempt received for token [{}]", token);

  6. AuthenticationInfo info;

  7. try {

  8. info = doAuthenticate(token);

  9. if (info == null) {

  10. String msg = "No account information found for authentication token [" + token + "] by this " +

  11. "Authenticator instance. Please check that it is configured correctly.";

  12. throw new AuthenticationException(msg);

  13. }

  14. } catch (Throwable t) {

  15. AuthenticationException ae = null;

  16. if (t instanceof AuthenticationException) {

  17. ae = (AuthenticationException) t;

  18. }

  19. if (ae == null) {

  20. //Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more

  21. //severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate:

  22. String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " +

  23. "error? (Typical or expected login exceptions should extend from AuthenticationException).";

  24. ae = new AuthenticationException(msg, t);

  25. if (log.isWarnEnabled())

  26. log.warn(msg, t);

  27. }

  28. try {

  29. notifyFailure(token, ae);

  30. } catch (Throwable t2) {

  31. if (log.isWarnEnabled()) {

  32. String msg = "Unable to send notification for failed authentication attempt - listener error?. " +

  33. "Please check your AuthenticationListener implementation(s). Logging sending exception " +

  34. "and propagating original AuthenticationException instead...";

  35. log.warn(msg, t2);

  36. }

  37. }

  38. throw ae;

  39. }

  40. log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);

  41. notifySuccess(token, info);

  42. return info;

  43. }

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

  1. protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {

  2. assertRealmsConfigured();

  3. Collection<Realm> realms = getRealms();

  4. if (realms.size() == 1) {

  5. return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);

  6. } else {

  7. return doMultiRealmAuthentication(realms, authenticationToken);

  8. }

  9. }

  10. protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {

  11. if (!realm.supports(token)) {

  12. String msg = "Realm [" + realm + "] does not support authentication token [" +

  13. token + "]. Please ensure that the appropriate Realm implementation is " +

  14. "configured correctly or that the realm accepts AuthenticationTokens of this type.";

  15. throw new UnsupportedTokenException(msg);

  16. }

  17. AuthenticationInfo info = realm.getAuthenticationInfo(token);

  18. if (info == null) {

  19. String msg = "Realm [" + realm + "] was unable to find account data for the " +

  20. "submitted AuthenticationToken [" + token + "].";

  21. throw new UnknownAccountException(msg);

  22. }

  23. return info;

  24. }

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

  1. public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

  2. AuthenticationInfo info = getCachedAuthenticationInfo(token);

  3. if (info == null) {

  4. //otherwise not cached, perform the lookup:

  5. info = doGetAuthenticationInfo(token);

  6. log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);

  7. if (token != null && info != null) {

  8. cacheAuthenticationInfoIfPossible(token, info);

  9. }

  10. } else {

  11. log.debug("Using cached authentication info [{}] to perform credentials matching.", info);

  12. }

  13. if (info != null) {

  14. assertCredentialsMatch(token, info);

  15. } else {

  16. log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);

  17. }

  18. return info;

  19. }

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

  1. /**

  2. * 认证

  3. */

  4. @Override

  5. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

  6. StatelessToken statelessToken = (StatelessToken) token;

  7. String username = (String) statelessToken.getPrincipal();

  8. //密码,shiro会根据token的credentials进行加密然后与SimpleAuthenticationInfo的第二个参数进行比较

  9. //String loginedToken = (String) statelessToken.getCredentials();

  10. //通过校验

  11. SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(

  12. username,

  13. statelessToken.getToken(),

  14. ByteSource.Util.bytes(statelessToken.getSalt()),

  15. getName()

  16. );

  17. return authenticationInfo;

  18. }

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

  1. AuthenticationInfo info = authenticate(token);

  2. Subject loggedIn = createSubject(token, info, subject);

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

注解的校验

注解的启用方式

  1. @Bean

  2. public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {

  3. AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();

  4. authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);

  5. return authorizationAttributeSourceAdvisor;

  6. }

  7. public AuthorizationAttributeSourceAdvisor() {

  8. setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());

  9. }

关于 AuthorizationAttributeSourceAdvisor:

  1. public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {

  2. private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class);

  3. private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =

  4. new Class[] {

  5. RequiresPermissions.class, RequiresRoles.class,

  6. RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class

  7. };

  8. public boolean matches(Method method, Class targetClass) {

  9. Method m = method;

  10. if ( isAuthzAnnotationPresent(m) ) {

  11. return true;

  12. }

  13. //The 'method' parameter could be from an interface that doesn't have the annotation.

  14. //Check to see if the implementation has it.

  15. if ( targetClass != null) {

  16. try {

  17. m = targetClass.getMethod(m.getName(), m.getParameterTypes());

  18. return isAuthzAnnotationPresent(m) || isAuthzAnnotationPresent(targetClass);

  19. } catch (NoSuchMethodException ignored) {

  20. //default return value is false. If we can't find the method, then obviously

  21. //there is no annotation, so just use the default return value.

  22. }

  23. }

  24. return false;

  25. }

  26. private boolean isAuthzAnnotationPresent(Class<?> targetClazz) {

  27. for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {

  28. Annotation a = AnnotationUtils.findAnnotation(targetClazz, annClass);

  29. if ( a != null ) {

  30. return true;

  31. }

  32. }

  33. return false;

  34. }

  35. private boolean isAuthzAnnotationPresent(Method method) {

  36. for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {

  37. Annotation a = AnnotationUtils.findAnnotation(method, annClass);

  38. if ( a != null ) {

  39. return true;

  40. }

  41. }

  42. return false;

  43. }

这里简要的说以下几点:

  • 继承自 StaticMethodMatcherPointcutAdvisor,可以对方法进行 aop 切面编程。

  • matches 方法会对方法上加有 AUTHZANNOTATIONCLASSES 列表中的注解的方法进行切面。

关于 AopAllianceAnnotationsAuthorizingMethodInterceptor:

  1. public AopAllianceAnnotationsAuthorizingMethodInterceptor() {

  2. List<AuthorizingAnnotationMethodInterceptor> interceptors =

  3. new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);

  4. //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the

  5. //raw JDK resolution process.

  6. AnnotationResolver resolver = new SpringAnnotationResolver();

  7. //we can re-use the same resolver instance - it does not retain state:

  8. interceptors.add(new RoleAnnotationMethodInterceptor(resolver));

  9. interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));

  10. interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));

  11. interceptors.add(new UserAnnotationMethodInterceptor(resolver));

  12. interceptors.add(new GuestAnnotationMethodInterceptor(resolver));

  13. setMethodInterceptors(interceptors);

  14. }

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

我们以 AuthenticatedAnnotationMethodInterceptor 为例:

  1. public AuthenticatedAnnotationMethodInterceptor(AnnotationResolver resolver) {

  2. super(new AuthenticatedAnnotationHandler(), resolver);

  3. }

AuthenticatedAnnotationHandler:

  1. public AuthenticatedAnnotationHandler() {

  2. super(RequiresAuthentication.class);

  3. }

  4. public void assertAuthorized(Annotation a) throws UnauthenticatedException {

  5. if (a instanceof RequiresAuthentication && !getSubject().isAuthenticated() ) {

  6. throw new UnauthenticatedException( "The current Subject is not authenticated. Access denied." );

  7. }

  8. }

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

我们再看下 PermissionAnnotationMethodInterceptor:

  1. public PermissionAnnotationMethodInterceptor() {

  2. super( new PermissionAnnotationHandler() );

  3. }

  4. public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) {

  5. super( new PermissionAnnotationHandler(), resolver);

  6. }

PermissionAnnotationHandler:

  1. protected String[] getAnnotationValue(Annotation a) {

  2. RequiresPermissions rpAnnotation = (RequiresPermissions) a;

  3. return rpAnnotation.value();

  4. }

  5. public void assertAuthorized(Annotation a) throws AuthorizationException {

  6. if (!(a instanceof RequiresPermissions)) return;

  7. RequiresPermissions rpAnnotation = (RequiresPermissions) a;

  8. String[] perms = getAnnotationValue(a);

  9. Subject subject = getSubject();

  10. if (perms.length == 1) {

  11. subject.checkPermission(perms[0]);

  12. return;

  13. }

  14. if (Logical.AND.equals(rpAnnotation.logical())) {

  15. getSubject().checkPermissions(perms);

  16. return;

  17. }

  18. if (Logical.OR.equals(rpAnnotation.logical())) {

  19. // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first

  20. boolean hasAtLeastOnePermission = false;

  21. for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true;

  22. // Cause the exception if none of the role match, note that the exception message will be a bit misleading

  23. if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]);

  24. }

  25. }

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

  1. public void checkPermission(String permission) throws AuthorizationException {

  2. assertAuthzCheckPossible();

  3. securityManager.checkPermission(getPrincipals(), permission);

  4. }

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

  1. public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException {

  2. this.authorizer.checkPermission(principals, permission);

  3. }

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

  1. public void checkPermission(PrincipalCollection subjectIdentifier, String permission) throws AuthorizationException {

  2. Permission p = getPermissionResolver().resolvePermission(permission);

  3. checkPermission(subjectIdentifier, p);

  4. }

  5. public void checkPermission(PrincipalCollection principal, Permission permission) throws AuthorizationException {

  6. AuthorizationInfo info = getAuthorizationInfo(principal);

  7. checkPermission(permission, info);

  8. }

  9. org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo:

  10. protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {

  11. if (principals == null) {

  12. return null;

  13. }

  14. AuthorizationInfo info = null;

  15. if (log.isTraceEnabled()) {

  16. log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");

  17. }

  18. Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();

  19. if (cache != null) {

  20. if (log.isTraceEnabled()) {

  21. log.trace("Attempting to retrieve the AuthorizationInfo from cache.");

  22. }

  23. Object key = getAuthorizationCacheKey(principals);

  24. info = cache.get(key);

  25. if (log.isTraceEnabled()) {

  26. if (info == null) {

  27. log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");

  28. } else {

  29. log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");

  30. }

  31. }

  32. }

  33. if (info == null) {

  34. // Call template method if the info was not found in a cache

  35. info = doGetAuthorizationInfo(principals);

  36. // If the info is not null and the cache has been created, then cache the authorization info.

  37. if (info != null && cache != null) {

  38. if (log.isTraceEnabled()) {

  39. log.trace("Caching authorization info for principals: [" + principals + "].");

  40. }

  41. Object key = getAuthorizationCacheKey(principals);

  42. cache.put(key, info);

  43. }

  44. }

  45. return info;

  46. }

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

  1. com.shimh.oauth.OAuthRealm#doGetAuthorizationInfo:

  2. //授权

  3. @Override

  4. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

  5. String account = (String) principals.getPrimaryPrincipal();

  6. User user = userService.getUserByAccount(account);

  7. SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

  8. Set<String> roles = new HashSet<String>();

  9. //简单处理 只有admin一个角色

  10. if (user.getAdmin()) {

  11. roles.add(BaseConstant.ROLE_ADMIN);

  12. }

  13. authorizationInfo.setRoles(roles);

  14. return authorizationInfo;

  15. }

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

RoleAnnotationMethodInterceptor:

  1. public RoleAnnotationMethodInterceptor(AnnotationResolver resolver) {

  2. super(new RoleAnnotationHandler(), resolver);

  3. }

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

  1. public RoleAnnotationHandler() {

  2. super(RequiresRoles.class);

  3. }

  4. public void assertAuthorized(Annotation a) throws AuthorizationException {

  5. if (!(a instanceof RequiresRoles)) return;

  6. RequiresRoles rrAnnotation = (RequiresRoles) a;

  7. String[] roles = rrAnnotation.value();

  8. if (roles.length == 1) {

  9. getSubject().checkRole(roles[0]);

  10. return;

  11. }

  12. if (Logical.AND.equals(rrAnnotation.logical())) {

  13. getSubject().checkRoles(Arrays.asList(roles));

  14. return;

  15. }

  16. if (Logical.OR.equals(rrAnnotation.logical())) {

  17. // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first

  18. boolean hasAtLeastOneRole = false;

  19. for (String role : roles) if (getSubject().hasRole(role)) hasAtLeastOneRole = true;

  20. // Cause the exception if none of the role match, note that the exception message will be a bit misleading

  21. if (!hasAtLeastOneRole) getSubject().checkRole(roles[0]);

  22. }

  23. }

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

  1. public void checkRole(String role) throws AuthorizationException {

  2. assertAuthzCheckPossible();

  3. securityManager.checkRole(getPrincipals(), role);

  4. }

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

  1. public void checkRole(PrincipalCollection principals, String role) throws AuthorizationException {

  2. this.authorizer.checkRole(principals, role);

  3. }

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

  1. public void checkRole(PrincipalCollection principal, String role) throws AuthorizationException {

  2. AuthorizationInfo info = getAuthorizationInfo(principal);

  3. checkRole(role, info);

  4. }

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

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

ArticleController 的发布方法如下:

  1. @PostMapping("/publish")

  2. @RequiresAuthentication

  3. @LogAnnotation(module = "文章", operation = "发布文章")

  4. @AuthPassport(noNeed = true)

  5. public Result saveArticle(@Validated @RequestBody Article article, @CurrentUser UserTo userTo) {

  6. Integer articleId = articleService.publishArticle(article,userTo);

  7. Result r = Result.success();

  8. r.simple().put("articleId", articleId);

  9. return r;

  10. }

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

  1. public Object invoke(MethodInvocation methodInvocation) throws Throwable {

  2. org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation);

  3. return super.invoke(mi);

  4. }

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

  1. public Object invoke(MethodInvocation methodInvocation) throws Throwable {

  2. assertAuthorized(methodInvocation);

  3. return methodInvocation.proceed();

  4. }

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

  1. protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException {

  2. //default implementation just ensures no deny votes are cast:

  3. Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();

  4. if (aamis != null && !aamis.isEmpty()) {

  5. for (AuthorizingAnnotationMethodInterceptor aami : aamis) {

  6. if (aami.supports(methodInvocation)) {

  7. aami.assertAuthorized(methodInvocation);

  8. }

  9. }

  10. }

  11. }

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

  1. public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {

  2. try {

  3. ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi));

  4. }

  5. catch(AuthorizationException ae) {

  6. // Annotation handler doesn't know why it was called, so add the information here if possible.

  7. // Don't wrap the exception here since we don't want to mask the specific exception, such as

  8. // UnauthenticatedException etc.

  9. if (ae.getCause() == null) ae.initCause(new AuthorizationException("Not authorized to invoke method: " + mi.getMethod()));

  10. throw ae;

  11. }

  12. }

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

  1. public void assertAuthorized(Annotation a) throws UnauthenticatedException {

  2. if (a instanceof RequiresAuthentication && !getSubject().isAuthenticated() ) {

  3. throw new UnauthenticatedException( "The current Subject is not authenticated. Access denied." );

  4. }

  5. }

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

  1. protected Subject getSubject() {

  2. return SecurityUtils.getSubject();

  3. }

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

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

  1. protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)

  2. throws ServletException, IOException {

  3. Throwable t = null;

  4. try {

  5. final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);

  6. final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

  7. final Subject subject = createSubject(request, response);

  8. //noinspection unchecked

  9. subject.execute(new Callable() {

  10. public Object call() throws Exception {

  11. updateSessionLastAccessTime(request, response);

  12. executeChain(request, response, chain);

  13. return null;

  14. }

  15. });

  16. } catch (ExecutionException ex) {

  17. t = ex.getCause();

  18. } catch (Throwable throwable) {

  19. t = throwable;

  20. }

  21. if (t != null) {

  22. if (t instanceof ServletException) {

  23. throw (ServletException) t;

  24. }

  25. if (t instanceof IOException) {

  26. throw (IOException) t;

  27. }

  28. //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:

  29. String msg = "Filtered request failed.";

  30. throw new ServletException(msg, t);

  31. }

  32. }

  33. protected WebSubject createSubject(ServletRequest request, ServletResponse response) {

  34. return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();

  35. }

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

  1. public WebSubject buildWebSubject() {

  2. Subject subject = super.buildSubject();

  3. if (!(subject instanceof WebSubject)) {

  4. String msg = "Subject implementation returned from the SecurityManager was not a " +

  5. WebSubject.class.getName() + " implementation. Please ensure a Web-enabled SecurityManager " +

  6. "has been configured and made available to this builder.";

  7. throw new IllegalStateException(msg);

  8. }

  9. return (WebSubject) subject;

  10. }

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

  1. public Subject buildSubject() {

  2. return this.securityManager.createSubject(this.subjectContext);

  3. }

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

  1. public Subject createSubject(SubjectContext subjectContext) {

  2. //create a copy so we don't modify the argument's backing map:

  3. SubjectContext context = copy(subjectContext);

  4. //ensure that the context has a SecurityManager instance, and if not, add one:

  5. context = ensureSecurityManager(context);

  6. //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before

  7. //sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the

  8. //process is often environment specific - better to shield the SF from these details:

  9. context = resolveSession(context);

  10. //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first

  11. //if possible before handing off to the SubjectFactory:

  12. context = resolvePrincipals(context);

  13. Subject subject = doCreateSubject(context);

  14. //save this subject for future reference if necessary:

  15. //(this is needed here in case rememberMe principals were resolved and they need to be stored in the

  16. //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).

  17. //Added in 1.2:

  18. save(subject);

  19. return subject;

  20. }

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

方案

方案一

加一个 filter 如下:

  1. public class JwtAuthFilter extends AuthenticatingFilter {

  2. /**

  3. * 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。

  4. */

  5. @Override

  6. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

  7. if(this.isLoginRequest(request, response))

  8. return true;

  9. boolean allowed = false;

  10. try {

  11. allowed = executeLogin(request, response);

  12. } catch(IllegalStateException e){ //not found any token

  13. log.error("Not found any token");

  14. }catch (Exception e) {

  15. log.error("Error occurs when login", e);

  16. }

  17. return allowed || super.isPermissive(mappedValue);

  18. }

  19. /**

  20. * 这里重写了父类的方法,使用我们自己定义的Token类,提交给shiro。这个方法返回null的话会直接抛出异常,进入isAccessAllowed()的异常处理逻辑。

  21. */

  22. @Override

  23. protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {

  24. //这里是存放在请求的header中的,也可以存放在cookie中

  25. String jwtToken = getAuthzHeader(servletRequest);

  26. if(StringUtils.isNotBlank(jwtToken)&&!JwtUtils.isTokenExpired(jwtToken))

  27. return new JWTToken(jwtToken);

  28. return null;

  29. }

  30. ..........

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

  1. protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {

  2. AuthenticationToken token = createToken(request, response);

  3. if (token == null) {

  4. String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +

  5. "must be created in order to execute a login attempt.";

  6. throw new IllegalStateException(msg);

  7. }

  8. try {

  9. Subject subject = getSubject(request, response);

  10. subject.login(token);

  11. return onLoginSuccess(token, subject, request, response);

  12. } catch (AuthenticationException e) {

  13. return onLoginFailure(token, e, request, response);

  14. }

  15. }

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

  1. protected Subject getSubject(ServletRequest request, ServletResponse response) {

  2. return SecurityUtils.getSubject();

  3. }

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

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

方案二

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

  1. Cookie[] cookies = request.getCookies();

  2. if (cookies != null) {

  3. Optional<Cookie> cookieOptional = Arrays.stream(cookies).filter(cookie -> BaseConstant.AUTH.equals(cookie.getName())).findFirst();

  4. if (cookieOptional.isPresent()) {

  5. Cookie cookie = cookieOptional.get();

  6. try {

  7. Map<String, Object> paramMap = JwtUtil.decode(BaseConstant.SHIRO_KEY, cookie.getValue(), BaseConstant.SHIRO_SALT);

  8. String userObj = (String)paramMap.get("user");

  9. UserTo userTo = JSONObject.parseObject(userObj, UserTo.class);

  10. Subject subject = SecurityUtils.getSubject();

  11. //调用了login之后会校验OAuthRealm的supports方法,查看token类型

  12. //注意,这里调用subject.login是为了给当前线程的subject进行认证操作,赋给principles和authorized属性值

  13. StatelessToken token = new StatelessToken(userTo.getAccount(),cookie.getValue(),BaseConstant.SHIRO_SALT);

  14. subject.login(token);

  15. return Optional.of(userTo);

  16. } catch (Exception e) {

  17. log.error("cookie的值为:" + cookie.getValue() + "解密失败!", e);

  18. }

  19. }

  20. }

  21. return Optional.empty();

其中 OAuthRealm 的代码如下:

  1. public class OAuthRealm extends AuthorizingRealm {

  2. private static final Logger LOG = LoggerFactory.getLogger(OAuthRealm.class);

  3. @Autowired

  4. private UserService userService;

  5. public OAuthRealm() {

  6. this.setCredentialsMatcher(new JWTCredentialsMatcher());

  7. }

  8. @Override

  9. public boolean supports(AuthenticationToken token) {

  10. //仅支持StatelessToken类型的

  11. return token instanceof StatelessToken;

  12. }

  13. //授权

  14. @Override

  15. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

  16. String account = (String) principals.getPrimaryPrincipal();

  17. User user = userService.getUserByAccount(account);

  18. SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

  19. Set<String> roles = new HashSet<String>();

  20. //简单处理 只有admin一个角色

  21. if (user.getAdmin()) {

  22. roles.add(BaseConstant.ROLE_ADMIN);

  23. }

  24. authorizationInfo.setRoles(roles);

  25. return authorizationInfo;

  26. }

  27. /**

  28. * 认证

  29. */

  30. @Override

  31. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

  32. StatelessToken statelessToken = (StatelessToken) token;

  33. String username = (String) statelessToken.getPrincipal();

  34. //密码,shiro会根据token的credentials进行加密然后与SimpleAuthenticationInfo的第二个参数进行比较

  35. //String loginedToken = (String) statelessToken.getCredentials();

  36. //通过校验

  37. SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(

  38. username,

  39. statelessToken.getCredentials(),

  40. ByteSource.Util.bytes(statelessToken.getSalt()),

  41. getName()

  42. );

  43. return authenticationInfo;

  44. }

  45. }

JWTCredentialsMatcher 代码如下:

  1. public class JWTCredentialsMatcher implements CredentialsMatcher{

  2. /**

  3. * 这里只是简单实现下,可以使用jwt的verify方法来校验

  4. * @param token

  5. * @param info

  6. * @return

  7. */

  8. @Override

  9. public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {

  10. if (info instanceof SimpleAuthenticationInfo){

  11. SimpleAuthenticationInfo simpleAuthenticationInfo = (SimpleAuthenticationInfo) info;

  12. PrincipalCollection principals = info.getPrincipals();

  13. if (principals.isEmpty()){

  14. return false;

  15. }

  16. if (token instanceof StatelessToken){

  17. String primaryPrincipal = (String) principals.getPrimaryPrincipal();

  18. String tokenPrincipal = (String) token.getPrincipal();

  19. String credentials = (String) info.getCredentials();

  20. String credentials1 = (String) token.getCredentials();

  21. String toHex = simpleAuthenticationInfo.getCredentialsSalt().toHex();

  22. StatelessToken statelessToken = (StatelessToken) token;

  23. String toHex1 = ByteSource.Util.bytes(statelessToken.getSalt()).toHex();

  24. if (primaryPrincipal.equals(tokenPrincipal) && credentials.equals(credentials1) && toHex.equals(toHex1)){

  25. return true;

  26. }

  27. return false;

  28. }else {

  29. return false;

  30. }

  31. }

  32. return false;

  33. }

  34. }

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

亲测有效哦

参考

  • https://www.jianshu.com/p/0b1131be7ace


文章转载自开发架构二三事,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论