shiro

shiro:

1、介绍

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。

2、组成

img

 

img

shiro包含三个核心组件:Subject,SecurityManager,Realms
其中:
subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证,授权。
securityManager:安全管理器,主体进行认证和授权都是通过securityManager进行。
authenticator:认证器,用户身份验证组件,登录控制。
authorizer:授权器,主体进行授权最终通过authorizer进行的。
sessionManager:shiro提供的session的管理的方式
SessionDao:通过SessionDao管理Session数据,使用与存储。
cacheManager:缓存管理器,对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
relam:域,相当于数据源,通过relam存取认证,授权相关数据。
cryptography:密码管理,提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

3、登录功能的实现(包含免密登录)

登录流程:
1、应用程序构建了一个终端用户认证信息的AuthenticationToken实例后,调用Subject.login方法
2、Sbuject会委托应用程序设置的securityManager实例调用securityManager.login(token)方法。
3、SecurityManager接受到token(令牌)信息后会委托内置的Authenticator的实例(通常都是ModularRealmAuthenticator类的实例)调用authenticator.authenticate(token).ModularRealmAuthenticator在认证过程中会对设置的一个或多个Realm实例进行适配,它实际上为Shiro提供了一个可拔插的认证机制。
4、如果在应用程序中配置了多个Realm,ModularRealmAuthenticator会根据配置的AuthenticationStrategy(认证策略)来进行多Realm的认证过程。在Realm被调用后,AuthenticationStrategy将对每一个Realm的结果作出响应。 
注:如果应用程序中仅配置了一个Realm,Realm将被直接调用而无需再配置认证策略。 
5、Realm将调用getAuthenticationInfo(token);getAuthenticationInfo方法就是实际认证处理,我们通过覆盖Realm的doGetAuthenticationInfo方法来编写我们自定义的认证处理。

img

 

代码实现:

Controller:
@PostMapping("/login")
public String login(HttpSession session, @RequestBody User user) throws Exception {

    String username = user.getUsername();
    String companyName = user.getCompanyName();
    username = HtmlUtils.htmlEscape(username);
    User serviceByUsername = null;
    //更具用户名查询用户
    serviceByUsername = userService.findByUsername(username);

Subject subject = SecurityUtils.getSubject();
    String decodePassword = CryptTool.decodePw(user.getPassword());
//因为业务需求不用,所以这里用用户Id和密码作为UsernamePasswordToken的参数
    UsernamePasswordToken token = new UsernamePasswordToken(serviceByUsername.getId(), decodePassword);
    token.setRememberMe(true);

    try {
        subject.login(token);
        return jsonSuccess(byUsername, "登录成功");
    } catch (AuthenticationException e) {
        logger.error("登录失败:"+e.toString());
        return jsonFailure("104", "登录失败,请联系管理员");
    }
}

subject.login(token)的源码:

 

 

 

shiro目录结构:(其中AuthenticationController用于登录)

 

自己代码实现:

ShiroConfiguration


package cn.com.rivercloud.secutity.config;

import cn.com.rivercloud.secutity.filter.URLPathMatchingFilter;
import cn.com.rivercloud.secutity.matcher.MyRetryLimitCredentialsMatcher;
import cn.com.rivercloud.secutity.relam.SystemRelam;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.mgt.SecurityManager;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* author shichangle
*
*/
@Configuration
public class ShiroConfiguration {

   /**
    * shiro生命周期处理器
    *
    * @return
    */
   @Bean
   public static LifecycleBeanPostProcessor getLifecycleBeanProcessor() {
       return new LifecycleBeanPostProcessor();
  }

   @Bean(name = "sessionDAO")
   public MemorySessionDAO getMemorySessionDao() {
       return new MemorySessionDAO();
  }

   @Bean(name = "simpleIdCookie")
   public SimpleCookie getSimpleCookie() {
       SimpleCookie simpleCookie = new SimpleCookie();
       simpleCookie.setName("SHIROSESSIONID");
       return simpleCookie;
  }

   //配置shirosession的一个管理器
   @Bean(name = "sessionManager")
   public DefaultWebSessionManager getDefaultWebSessionManager(@Qualifier("sessionDAO") MemorySessionDAO sessionDAO,
                                                               @Qualifier("simpleIdCookie") SimpleCookie simpleCookie) {
       DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
       sessionManager.setSessionDAO(sessionDAO);
       sessionManager.setSessionIdCookie(simpleCookie);
       //设置session过期时间 1800s
       sessionManager.setGlobalSessionTimeout(1800000L);
       return sessionManager;
  }

   //配置session的缓存管理器
   @Bean(name = "shiroCacheManager")
   public MemoryConstrainedCacheManager getMemoryConstrainedCacheManager() {
       return new MemoryConstrainedCacheManager();
  }


   @Bean
   public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
       //安全事务管理器工厂类
       ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
       shiroFilterFactoryBean.setSecurityManager(securityManager);
       //shiroFilterFactoryBean.setLoginUrl("/index");//未登录时拦截的路径

       //配置访问权限
       Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
       //自定义个过滤器设置1
       Map<String, Filter> customizedFilter = new HashMap<>();
       //自定义过滤器配置2 ,命名,需要设置在过滤路径前

       //自定义过滤器设置4 启用
       customizedFilter.put("url", getURLPathMatchingFilter());
       shiroFilterFactoryBean.setFilters(customizedFilter);
       shiroFilterFactoryBean.setUnauthorizedUrl("/csm/api/v1.0/login");
       filterChainDefinitionMap.put("/**", "url");
       shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

       return shiroFilterFactoryBean;
  }

   public URLPathMatchingFilter getURLPathMatchingFilter() {
       return new URLPathMatchingFilter();
  }

   //配置核心安全事务管理器
   @Bean
   public SecurityManager securityManager(@Qualifier("shiroCacheManager") MemoryConstrainedCacheManager shiroCacheManager,
                                          @Qualifier("sessionManager") DefaultWebSessionManager sessionManager) {
       DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
       securityManager.setRealm(getRealm());
       securityManager.setRememberMeManager(rememberMeManager());
       securityManager.setCacheManager(shiroCacheManager);
       securityManager.setSessionManager(sessionManager);//设置过期时间

       return securityManager;
  }

   public CookieRememberMeManager rememberMeManager() {
       CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
       cookieRememberMeManager.setCookie(rememberMeCookie());
       cookieRememberMeManager.setCipherKey("EVANNIGHTLY_WAOU".getBytes());
       return cookieRememberMeManager;
  }

   @Bean
   public SimpleCookie rememberMeCookie() {
       SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
       simpleCookie.setMaxAge(86400);//存活 一天
       return simpleCookie;
  }

   //自定义权限登录器
   @Bean
   public SystemRelam getRealm() {
       SystemRelam systemRelam = new SystemRelam();
       systemRelam.setCredentialsMatcher(hashedCredentialsMatcher());
       return systemRelam;
  }


   @Bean
   public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
       AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
       authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
       return authorizationAttributeSourceAdvisor;
  }


   //以下用于免登陆
   @Bean(name = "myRetryLimitCredentialsMatcher")
   public MyRetryLimitCredentialsMatcher hashedCredentialsMatcher(){
       MyRetryLimitCredentialsMatcher hashedCredentialsMatcher = new MyRetryLimitCredentialsMatcher();
       // 采用MD5方式加密
       hashedCredentialsMatcher.setHashAlgorithmName("md5");
       // 设置加密次数
       hashedCredentialsMatcher.setHashIterations(2);
       return hashedCredentialsMatcher;
  }

}



URLPathMatchingFilter:

package cn.com.rivercloud.secutity.filter;



import cn.com.rivercloud.modal.system.User;
import cn.com.rivercloud.secutity.domain.ResultData;
import cn.com.rivercloud.user.service.PermissionService;
import cn.com.rivercloud.user.service.RoleService;
import cn.com.rivercloud.user.service.UserService;
import cn.com.rivercloud.util.SpringContextUtils;
import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.apache.shiro.web.servlet.ShiroHttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;


import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;


/**
* 所有接口前置过滤类
* author shichangle
* date 2019/12/18 0018 18:11
*/
@SuppressWarnings("all")
@Configuration
public class URLPathMatchingFilter extends PathMatchingFilter {

   private static final Logger logger = LoggerFactory.getLogger(URLPathMatchingFilter.class);

   @Autowired
   private UserService userService;
   @Autowired
   private RoleService roleService;
   @Autowired
   private PermissionService permissionService;
   @Autowired
   private SessionDAO sessionDAO;
   @Autowired
   private SessionManager sessionManager;
   @Autowired
   private MemoryConstrainedCacheManager cacheManager;

   @Override
   protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {

       HttpServletRequest httpServletRequest = (HttpServletRequest) request;
       HttpServletResponse httpServletResponse = (HttpServletResponse) response;

       if (null == userService) {
           userService = SpringContextUtils.getContext().getBean(UserService.class);
      }
       if (null == roleService) {
           roleService = SpringContextUtils.getContext().getBean(RoleService.class);
      }
       if (null == permissionService) {
           permissionService = SpringContextUtils.getContext().getBean(PermissionService.class);
      }
       if (null == cacheManager) {
           cacheManager = SpringContextUtils.getContext().getBean(MemoryConstrainedCacheManager.class);
      }

       if (HttpMethod.OPTIONS.toString().equals((httpServletRequest).getMethod())) {
           httpServletResponse.setStatus(HttpStatus.NO_CONTENT.value());
           return true;
      }
       String requestAPI = getPathWithinApplication(request);
       //指定接口放行,返回true
       //未放行的 判断登录   --未登录 返回登录页面
       //                   --登录了 判断该用户名对应的session 中的字段是否是新的
       //                             是 进行下一步
       //                             不是 进行重新登录


       //咱是放行
       if (containsApi(requestAPI)) {
           return true;
      }

       Subject subject = SecurityUtils.getSubject();
       //判断登录,没有登录,直接进行之后的流程
       if (!subject.isAuthenticated()) {
           //没有登录,重定向到指定页面
           logger.info("未登录:"+requestAPI);
           if (isAjax(request)) {
               httpServletResponse.setCharacterEncoding("UTF-8");
               httpServletResponse.setContentType("application/json");
               ResultData resultData = new ResultData();
               resultData.setCode(1);
               resultData.setCode(403);
               resultData.setMessage("您的登录状态已过期,请重新登录");
               httpServletResponse.getWriter().write(JSONObject.toJSON(resultData).toString());
          } else {
               //非ajax请求重定向为登录页面
               httpServletResponse.sendRedirect("/index.html");
          }
           return false;
      } else {
           //登录
           Session session = subject.getSession();
           logger.debug("==session时间设置:" + String.valueOf(session.getTimeout())
                   + "===========");
           String id = subject.getPrincipal().toString();
           logger.debug("===当前用户id:==" + id);
           Serializable sessionId = session.getId();
           logger.debug("===当前用户sessionId:==" + sessionId);

           User byUsername = userService.findById(username);

           Cache<Object, Object> cache = cacheManager.getCache(id);
           ShiroHttpSession cacheSession = (ShiroHttpSession) cache.get(id);
           String kickout = cacheSession.getAttribute("kickout").toString();

           //对比数据
           if ("2".equals(kickout)) {
               return true;
          } else {
               //重新登录
               logger.info("未登录:");
               if (isAjax(request)) {
                   httpServletResponse.setCharacterEncoding("UTF-8");
                   httpServletResponse.setContentType("application/json");
                   ResultData resultData = new ResultData();
                   resultData.setCode(1);
                   resultData.setCode(403);
                   resultData.setMessage("您的登录状态已过期,请重新登录");
                   httpServletResponse.getWriter().write(JSONObject.toJSON(resultData).toString());
              } else {
                   //非ajax请求重定向为登录页面
                   httpServletResponse.sendRedirect("/index.html");
              }
               return false;
          }
      }
  }


   private boolean containsApi(String requestApi) {

       List<String> likeList = new ArrayList<>();
       
       likeList.add("/static/");
       likeList.add("/css/");
       likeList.add("/img/");
       likeList.add("/js/");
       likeList.add("/loading/");
       likeList.add("/index.");
       likeList.add("/nav");
       likeList.add("/logo.");
       likeList.add("/avatar2.");

       for (String s : likeList) {
           if (requestApi.contains(s) ) {
               return true;
          }
      }
       for (String s : allList) {
           if (requestApi.equals(s) ) {
               return true;
          }
      }
       return false;
  }

   private boolean isAjax(ServletRequest request) {
       String header = ((HttpServletRequest) request).getHeader("X-Requested-With");
       if ("XMLHttpRequest".equalsIgnoreCase(header)) {
           return false;
      }
       return true;
  }
}


macher(重写,用于免密登录)
package cn.com.rivercloud.secutity.matcher;

import cn.com.rivercloud.secutity.token.LoginType;
import cn.com.rivercloud.secutity.token.UsernamePasswordLoginTypeToken;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.springframework.context.annotation.Configuration;

/**
* author shichangle
* date 2020/5/21 0021 15:08
*/
@Configuration
public class MyRetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
   @Override
   public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
       UsernamePasswordLoginTypeToken tk = null;
       try {
           tk = (UsernamePasswordLoginTypeToken) token;
      } catch (Exception e) {
           return super.doCredentialsMatch(token, info);
      }
       if(tk.getLoginType().equals(LoginType.NOPASSWD)){
           return true;
      }
       return super.doCredentialsMatch(token, info);
  }
}



relam:(创建的时候名称有点问题)
package cn.com.rivercloud.secutity.relam;

import cn.com.rivercloud.modal.system.User;
import cn.com.rivercloud.user.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

/**
* author shichangle
* date 2019/12/18 0018 11:27
*/
public class SystemRelam extends AuthorizingRealm {

   @Autowired
   private UserService userService;

   //获取授权信息方法
   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
       SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
       return s;
  }

   //获取认证信息,根据token获取用户名
   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
       String username = token.getPrincipal().toString();
       User map  = userService.findById(username);
       String passwordInDB = map.getPassword();
       String salt = map.getSalt();
       SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,passwordInDB, ByteSource.Util.bytes(salt),getName());
       return authenticationInfo;
  }
}

 

token:用于免密登录

package cn.com.rivercloud.secutity.token;

/**
* 免密登录标识
* author shichangle
* date 2020/5/21 0021 14:56
*/
public enum LoginType {
  PASSWORD("password"), // 密码登录
  NOPASSWD("nopassword"); // 免密登录

  private String code;// 状态值

  private LoginType(String code) {
      this.code = code;
  }
  public String getCode () {
      return code;
  }
}


package cn.com.rivercloud.secutity.token;

import org.apache.shiro.authc.UsernamePasswordToken;

/**
* UsernamePasswordToken子类,用于免登陆
* author shichangle
* date 2020/5/21 0021 14:47
*/
public class UsernamePasswordLoginTypeToken extends UsernamePasswordToken {

  private LoginType loginType;

  public UsernamePasswordLoginTypeToken() {
      super();
  }

  public UsernamePasswordLoginTypeToken(String username, String password, boolean rememberMe, String host, LoginType loginType) {
      super(username, password, rememberMe, host);
      this.loginType = loginType;
  }

  /**
    * 免密登录
    * @param username
    */
  public UsernamePasswordLoginTypeToken(String username){
      super(username, "", false, null);
      this.loginType = LoginType.NOPASSWD;
  }

  /**
    * 密码登录
    * @param username
    * @param pwd
    */
  public UsernamePasswordLoginTypeToken(String username, String pwd) {
      super(username, pwd, false, null);
      this.loginType = LoginType.PASSWORD;
  }

  public LoginType getLoginType() {
      return loginType;
  }

  public void setLoginType(LoginType loginType) {
      this.loginType = loginType;
  }
}

 

4、踢出用户的实现

实现原理:

1、登录成功后,将用户信息存入shiro的session中,设置指定状态
2、踢出用户,去缓存中查找指定用户信息,更改状态
3、过滤接口,每次请求都检查session中的信息状态,登录则放行,未登录则返回登录页面
1//登录信息存入缓存中
subject.login(token);
//登录成功,更改用户的登录状态,以及去掉第一次登录的标识
userService.updateLoginStatus(serviceByUsername.getId(), LOGINSTATUS);
User byUsername = userService.findById(serviceByUsername.getId());
byUsername.setPassword("");
byUsername.setSalt("");
logger.info(username + "登录成功");
User.setUser(session, serviceByUsername);

//登录信息存入缓存中
Session subsession = subject.getSession();
logger.debug("==session时间设置:" + String.valueOf(subsession.getTimeout())
                   + "===========");
String id = subject.getPrincipal().toString();
logger.debug("===当前用户id:==" + id);
Serializable sessionId = subsession.getId();
logger.debug("===当前用户sessionId:==" + sessionId);

subsession.setAttribute("kickout", "2");
Cache<Object, Object> cache = cacheManager.getCache(id);
cache.put(id, session);
return jsonSuccess(byUsername, "登录成功");

 

2、踢出用户

/**
 * 踢出用户
 *
 * @param sessionId
 */
   
 Autowired
 private MemoryConstrainedCacheManager cacheManager;
     
 @Override
 public void kinckUser(HttpSession session, String sessionId) {
     getSessionBysessionId(sessionId);
}

 //根据sesisonid获取单个session对象
 private void getSessionBysessionId(String sessionId) {
     Cache<Object, Object> test = cacheManager.getCache(sessionId);
     ShiroHttpSession cacheSession = (ShiroHttpSession) test.get(sessionId);
     Object kickout = cacheSession.getAttribute("kickout");
     System.out.println(kickout);
     cacheSession.setAttribute("kickout", "1");
}

 

3、在URLPathMatchingFilter中有对每个接口进行过滤验证。

 

 

免登陆接口:

 public String nologin(HttpSession session, @RequestBody User user) throws Exception {

       String username = user.getUsername();
       String companyName = user.getCompanyName();
       username = HtmlUtils.htmlEscape(username);

       Subject subject = SecurityUtils.getSubject();
       UsernamePasswordLoginTypeToken tk = new UsernamePasswordLoginTypeToken(username);

       tk.setRememberMe(true);

       try {
           subject.login(tks);
           //登录成功,更改用户的登录状态,以及去掉第一次登录的标识
           userService.updateLoginStatus(username, LOGINSTATUS);
           User byUsername = userService.findById(username);
           byUsername.setPassword("");
           byUsername.setSalt("");
           logger.info(username + "登录成功");
           User.setUser(session, user);
           //登录信息存入缓存中

           Session subsession = subject.getSession();
           logger.debug("==session时间设置:" + String.valueOf(subsession.getTimeout())
                   + "===========");
           String id = subject.getPrincipal().toString();
           logger.debug("===当前用户id:==" + id);
           Serializable sessionId = subsession.getId();
           logger.debug("===当前用户sessionId:==" + sessionId);

           subsession.setAttribute("kickout", "2");
           Cache<Object, Object> cache = cacheManager.getCache(id);
           cache.put(id, session);
           return jsonSuccess( "登录成功");
      } catch (AuthenticationException e) {

           return jsonFailure();
      }
  }

 5、设置session过期时间

SecurityUtils.getSubject().getSession().setTimeout(l);
注意,单位是毫秒
原文地址:https://www.cnblogs.com/notchangeworld/p/12941820.html