SpringBoot - Shiro 前后端分离

SpringBoot - Shiro 前后端分离配置

前言

因为业务场景变化,公司项目要转变为前后端分离项目,这里讲解如何配置 shiro 作为前后端分离项目使用

思路

  • 传递 sessionId 的方式

    以前的方式是 Shiro 从 cookie 拿到 sessionId 验证来维持会话。现在我们使用在 Ajax 的请求头中传递 sessionId,并且需要重写 Shiro 获取 sessionId 的方式。

  • 登录成功失败后的路由控制

    前后端分离的项目中,路由完全由前端控制,请求只负责传递数据和验证回话,所以我们只负责像前端传递 json 数据,告知认证成功或失败的结果就行。

解决方案

  • 传递 sessionId

    1. 重写获取 sessionId 的方法

      MySessionManager.java

      public class MySessionManager extends DefaultWebSessionManager {
      
         private static final String TOKEN = "token";
         private static final String REFERENCE_SESSION_ID_SOURCE = "Stateless requese";
      
         public MySessionManager() {
             super();
         }
      
         @Override
         protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
             String id = WebUtils.toHttp(request).getHeader(TOKEN);
      		// 如果请求头中有包含 sessionId 的 token
             if (!StringUtils.isEmpty(id)) {
                 request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCE_SESSION_ID_SOURCE);
                 request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
                 request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
                 return id;
             } else {
                 // 如果没有就按照 Shiro 默认的方法去取
                 return super.getSessionId(request, response);
             }
         }
      }
      
    2. 将我们自定义的 sessionManager 配置到 SecurityManager 中

      ShiroConfig.java

      	/**
           * 安全管理器
           */
          @Bean
          public SecurityManager securityManager() {
              DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
              securityManager.setRealm(userRealm());
              securityManager.setSessionManager(sessionManager());
              return securityManager;
          }
      
      	/**
      	* sessionDAO 的方式
      	*/
          @Bean
          public SessionDAO sessionDAO(){
              return new MemorySessionDAO();
          }
      
          /**
           * 自定义 session 管理器
           */
          @Bean
          public SessionManager sessionManager() {
              MySessionManager mySessionManager = new MySessionManager();
              mySessionManager.setSessionDAO(sessionDAO());
              return mySessionManager;
          }
      
      
      
  • 认证成功或失败后的返回结果

    1. 自建一个没有登录的请求进行拦截,返回 json 信息,覆盖掉 shiro 原有跳转 login 页面的方式的类

      public class AjaxPermissionsAuthorizationFilter extends FormAuthenticationFilter {
      
          /**
           * Processes requests where the subject was denied access as determined by the isAccessAllowed method.
           * 被登陆拒绝后的请求
           * @param request
           * @param response
           * @return
           * @throws Exception
           */
          @Override
          protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
      
              JSONObject jsonObject = new JSONObject();
              jsonObject.put("code", 1);
              jsonObject.put("msg", "未登录");
              PrintWriter out = null;
              try {
                  response.setCharacterEncoding("UTF-8");
                  response.setContentType("application/json");
                  out = response.getWriter();
                  out.print(jsonObject);
              } catch (Exception e) {
      
              } finally {
                  if(null != out) {
                      out.flush();
                      out.close();
                  }
              }
              return false;
          }
      
          @Bean
          public FilterRegistrationBean registration(AjaxPermissionsAuthorizationFilter filter) {
              FilterRegistrationBean registration = new FilterRegistrationBean(filter);
              registration.setEnabled(false);
              return registration;
          }
      
      }
      

      当然你也可以使用定义一个未登录的请求去返回 json 信息

    2. 将 Shiro 默认认证失败跳转 login.jsp 的方式替换为上述 Filter,并且将 login 登录的请求配置为可以匿名访问

      ShiroConfig.java

          @Bean
          ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
      
              ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
              shiroFilterFactoryBean.setSecurityManager(securityManager);
              Map<String, Filter> filterMap = new LinkedHashMap<>();
              // 将 Shiro 默认认证失败跳转 login.jsp 的方式替换
              filterMap.put("authc", new AjaxPermissionsAuthorizationFilter());
              shiroFilterFactoryBean.setFilters(filterMap);
              LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
              filterChainDefinitionMap.put("/logout", "logout");
              // 将 login 登录的请求配置为可以匿名访问,如果不匿名总会被认为未登录拦截
              filterChainDefinitionMap.put("/login", "anon");
              filterChainDefinitionMap.put("/", "anon");
              filterChainDefinitionMap.put("/**", "authc");
              shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
              return shiroFilterFactoryBean;
          }
      
  • 其它部分代码

    • LoginController.java

      @Log("登录")
      	@PostMapping("/login")
      	@ResponseBody
      	R ajaxLogin(String mobile, String password) {
      
      		Subject subject = SecurityUtils.getSubject();
      		UsernamePasswordToken token = new UsernamePasswordToken(mobile, password);
      
      		try {
      			subject.login(token);
      			R r = new R();
      			r.put("token", subject.getSession().getId());
      			return r;
      		} catch (AuthenticationException e) {
      			return R.error(e.toString());
      		}
      	}
      
    • UserRealm.java

      @Override
      	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
      
      		String mobile = (String) token.getPrincipal();
      		String password = new String((char[]) token.getCredentials());
      
      		if (mobile.equals("15512345678")  && password .equals("12345") ) {
      			SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(mobile, password, getName());
      			return info;
      		} else {
      			throw new UnknownAccountException("账号密码不正确");
      		}
      	}
      
      }
      
    • TestController.java 用来检验是否配置正确

      @RequestMapping("/test")
      @Controller
      public class TestController {
      
          @GetMapping("/testToken")
          @ResponseBody
          R testToken() {
              return R.ok();
          }
      
      }
      

测试

验证登录成功

1554647537343

验证已登录的请求

1554647836998

验证账户密码不正确

1554647611245

验证未登录的请求

1554647748756

# ARTS 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×