springboot集成shiro

springboot集成shiro

1.shiro的相关介绍

shiro是Apache下的一个开源项目,是一款简单易用的的安全框架,提供了认证、授权、加密、会话管理,与spring Security 一样都是做一个权限的安全框架,但是与Spring Security 相比,在于 Shiro 使用了比较简单易懂易于使用的授权方式。shiro属于轻量级框架,相对于security简单的多,也没有security那么复杂。所以我这里也是简单介绍一下shiro的使用。其主要模块如下:

Authentication身份认证/登录,验证用户是不是拥有相应的身份;

Authorization授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

Cryptography加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web SupportWeb支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrencyshiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing提供测试支持;

Run As允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。

2.运行原理

1) Subject:主体,代表了当前“用户”。这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等。所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager。我们可以把 Subject 认为是一个门面,SecurityManager 才是实际的执行者。

2) SecurityManager:安全管理器。即所有与安全有关的操作都会与 SecurityManager 交互,且它管理着所有 Subject。可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,我们可以把它看成 DispatcherServlet 前端控制器。

3) Realm:域。Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法,也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作。我们可以把 Realm 看成 DataSource,即安全数据源。

3.Shiro 运行原理图2(Shiro 内部架构角度)如下:

1) Subject:主体,可以看到主体可以是任何与应用交互的“用户”。

2) SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher。它是 Shiro 的核心,所有具体的交互都通过 SecurityManager 进行控制。它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

3) Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,我们可以自定义实现。其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了。

4) Authrizer:授权器,或者访问控制器。它用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能。

5) Realm:可以有1个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的。它可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等。

6) SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 需要有人去管理它的生命周期,这个组件就是 SessionManager。而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境。

7) SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD。我们可以自定义 SessionDAO 的实现,控制 session 存储的位置。如通过 JDBC 写到数据库或通过 jedis 写入 redis 中。另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能。

8) CacheManager:缓存管理器。它来管理如用户、角色、权限等的缓存的。因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。

9) Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密的。

在第五点上我们说了realm可以用户自定义实现,接下来我们就讲一下用户该如何在springboot下自定义实现realm

(1)首先在pom文件中引入相关联的依赖

 <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
       <version>3.6</version>
 </dependency>
 <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring-boot-web-starter</artifactId>
 </dependency>

(2)自定义realm时要继承AuthorizingRealm类,重写其中的doGetAuthenticationInfo()方法和doGetAuthorizationInfo()方法,其中前者是用来登录验证其权限的,后者是对用户进行授权的,代码如下

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private MenuService menuService;

    //告诉shiro如何根据获取到的用户信息中的密码和盐值来校验密码
    {
        //设置用于匹配密码的CredentialsMatcher
        this.setCredentialsMatcher(new CustomCredentialsMatcher());
    }



    //定义如何获取用户信息的业务逻辑,给shiro做登录
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws
        AuthenticationException {

        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();

        // Null username is invalid
        if (username == null) {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }

        User userDB = userService.findBy("username",username);

        if (userDB == null) {
            throw new UnknownAccountException("No account found for admin [" + username + "]");
        }
        byte[] salt = EncodeUtils.decodeHex(userDB.getSalt());
        SimpleAuthenticationInfo
            info = new SimpleAuthenticationInfo(userDB, userDB.getPassword(), getName());
        if (userDB.getSalt() != null) {
            info.setCredentialsSalt(ByteSource.Util.bytes(salt));
        }
        return info;

    }
    /**
     * 此方法调用  hasRole,hasPermission的时候才会进行回调.
     *
     * 权限信息.(授权):
     * 1、如果用户正常退出,缓存自动清空;
     * 2、如果用户非正常退出,缓存自动清空;
     * 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。
     * (需要手动编程进行实现;放在service进行调用)
     * 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,
     * 调用clearCached方法;
     * :Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
        throws AuthenticationException {
        /*
         * 当没有使用缓存的时候,不断刷新页面的话,这个代码会不断执行,
         * 当其实没有必要每次都重新设置权限信息,所以我们需要放到缓存中进行管理;
         * 当放到缓存中时,这样的话,doGetAuthorizationInfo就只会执行一次了,
         * 缓存过期之后会再次执行。
         */
        log.info("后台权限校验-->CustomRealm.doGetAuthorizationInfo()");

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principals.getPrimaryPrincipal();
        Set<String> menus = null;
        if (user.getSystem()) {
            menus = menuService.getAllMenuCode();
        } else {
            menus = menuService.findMenuCodeByUserId(user.getId());
        }
        authorizationInfo.setStringPermissions(menus);
        Set<String> roles = roleService.getRolesByUserId(user.getId());
        authorizationInfo.setRoles(roles);
        return authorizationInfo;
    }

    /**
     * 指定principalCollection 清除
     */
    @Override public void clearCachedAuthorizationInfo(PrincipalCollection principalCollection) {
        SimplePrincipalCollection principals = new SimplePrincipalCollection(
            principalCollection, getName());
        super.clearCachedAuthorizationInfo(principals);
    }
}

 

其中在controller中要进行登录用户名密码的获取,注意如果要自己重写filter中的登录成功拦截器时,此时登录页面input标签传递参数必须是username和password,否则程序不会执行你的登录成功拦截。

public String loginPost(@Valid ValidAdmin validAdmin, BindingResult bindingResult,
      RedirectAttributes redirectAttributes, HttpServletRequest request) {
    if (bindingResult.hasErrors()) {
      return "redirect:/admin/login";
    }
    String username = validAdmin.getUsername();
    UsernamePasswordToken
        token =
        new UsernamePasswordToken(validAdmin.getUsername(), validAdmin.getPassword(), false);
    Subject currentUser = SecurityUtils.getSubject();
    try {
      //在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
      //每个Realm都能在必要时对提交的AuthenticationTokens作出反应
      //所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
      log.info("对用户[" + username + "]进行登录验证..验证开始");
      currentUser.login(token);
      log.info("对用户[" + username + "]进行登录验证..验证通过");
    } catch (UnknownAccountException uae) {
      log.info("对用户[" + username + "]进行登录验证..验证未通过,未知账户");
      redirectAttributes.addFlashAttribute("message", "未知账户");
    } catch (IncorrectCredentialsException ice) {
      log.info("对用户[" + username + "]进行登录验证..验证未通过,错误的凭证");
      redirectAttributes.addFlashAttribute("message", "密码不正确");
    } catch (LockedAccountException lae) {
      log.info("对用户[" + username + "]进行登录验证..验证未通过,账户已锁定");
      redirectAttributes.addFlashAttribute("message", "账户已锁定");
    } catch (ExcessiveAttemptsException eae) {
      log.info("对用户[" + username + "]进行登录验证..验证未通过,错误次数过多");
      redirectAttributes.addFlashAttribute("message", "用户名或密码错误次数过多");
    } catch (AuthenticationException ae) {
      //通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
      log.info("对用户[" + username + "]进行登录验证..验证未通过");
      ae.printStackTrace();
      redirectAttributes.addFlashAttribute("message", "用户名或密码不正确");
    }
    //验证是否登录成功
    if (currentUser.isAuthenticated()) {
      log.info("用户[" + username + "]登录认证通过(这里可以进行一些认证通过后的一些系统参数初始化操作)");
      return "redirect:/admin/home";
    } else {
      token.clear();
      return "redirect:/admin/login";
    }
  }

 

3.实现自定义登录登出拦截

实现登录成功拦截,自定义表单拦截器

由onPreHandle方法为起点,先判断当前用户是否已经登陆,然后当前账号是否是当前用户最后登陆的,如果不是,则拒绝,onAccessDenied,重定向到登陆界面 auth认证,

protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        Subject subject=this.getSubject(request,response);
        return subject.isAuthenticated() && isCurrentAccount(subject) ||onAccessDenied(request,response);
    }

 

登陆走下面的方法,如果当前访问的路径是登陆路径,则执行登陆流程

 protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        if (this.isLoginRequest(request, response)){
            return this.executeLogin(request, response);
        }
        return super.onPreHandle(request, response, mappedValue);
    }
public boolean executeLogin(ServletRequest request, ServletResponse response) {
        AuthenticationToken token=this.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);
        }else {
            try {
                Subject subject=this.getSubject(request,response);
//                boolean existing = subject.getSession(false) != null;
//                if (!existing){
//                    subject.getSession(true);
//                }
                subject.login(token);
                return this.onLoginSuccess(token, subject, request, response);
            } catch (AuthenticationException var5) {
                return this.onLoginFailure(token, var5, request, response);
            }
        }
    }

登录成功后执行onLoginSuccess,我们可以在此处进行登录成功后的自定义操作,我这里地登录日志进行了存储

protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
			ServletResponse response) throws Exception {
		log.info("进入保存方法");
		// 当前登录的用户
		User user = (User) SecurityUtils.getSubject().getPrincipal();
		// IP
		HttpServletRequest req = (HttpServletRequest) request;
		String ip = StringUtils.getIpAddr(req);
		// 操作时间
		Date date = new Date();
		LoginLog log = new LoginLog();
		log.setUserId(new Long(user.getId()));
		log.setIp(ip);
		log.setRecordTime(date);
		if(loginLogService != null){
			loginLogService.save(log);
		}
		
		issueSuccessRedirect(request, response);
		return false;
	}

注意如果我们自定义了表单拦截器,此时登录表单的路径一定要设置成authc,否则我们自定义的会失效,同是如果将登录的路径设置成authc,可能我们想通过直接访问登录页面会出现重定向过多问题,此时我们也可以在上述实现类中设置我们的登录路径,若不设置系统将自动定位到login页面处

@Override
	public String getLoginUrl() {
		return "/admin/login";
	}

自定义退出登录

public class CustomLogoutFilter extends LogoutFilter {

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        log.debug("logoutFilter");
        Subject subject = getSubject(request, response);
        if (isPostOnlyLogout()) {
            if (!WebUtils.toHttp(request).getMethod().toUpperCase(Locale.ENGLISH).equals("POST")) {
                return onLogoutRequestNotAPost(request, response);
            }
        }
        String redirectUrl = getRedirectUrl(request, response, subject);
        try {
            PrincipalCollection principals = subject.getPrincipals();
            RealmSecurityManager securityManager =
                    (RealmSecurityManager) SecurityUtils.getSecurityManager();
            CustomRealm adminRealm = (CustomRealm) securityManager.getRealms().iterator().next();
            adminRealm.clearCachedAuthorizationInfo(principals);
            subject.logout();
        } catch (SessionException ise) {
            log.debug("Encountered session exception during logout.  This can generally safely be ignored.", ise);
        }
        issueRedirect(request, response, redirectUrl);
        return false;
    }
}

3.springboot配置上述拦截器

package com.jidian.unicomad.web.conf;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.jidian.unicomad.web.shiro.CustomFormAuthenticationFilter;
import com.jidian.unicomad.web.shiro.CustomRealm;
import com.jidian.unicomad.web.shiro.CustomLogoutFilter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class ShiroConfig {

    /**
     *     注入自定义的realm,告诉shiro如何获取用户信息来做登录或权限控制
     */
    @Bean
    public Realm realm() {
        return new CustomRealm();
    }

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
         * 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
         * 加入这项配置能解决这个bug
         */
        creator.setUsePrefix(true);
        return creator;
    }

    ///**
    // * 这里统一做鉴权,即判断哪些请求路径需要用户登录,哪些请求路径不需要用户登录。
    // * 这里只做鉴权,不做权限控制,因为权限用注解来做。
    // * @return
    // */
    //@Bean
    //public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    //    DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
    //    chain.addPathDefinition("/login", "anon");
    //    chain.addPathDefinition("/logout", "logout");
    //    chain.addPathDefinition("/**", "authc");
    //    return chain;
    //}


    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SessionsSecurityManager securityManager) {
        log.debug("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        
        Map<String, Filter> filters = new HashMap<>();
        filters.put("logout", new CustomLogoutFilter());
        filters.put("authc", this.CustomFormAuthenticationFilter());
        shiroFilterFactoryBean.setFilters(filters);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

        //<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
        //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        filterChainDefinitionMap.put("/admin/login", "authc");
        filterChainDefinitionMap.put("/admin/logout", "logout");
        filterChainDefinitionMap.put("/admin/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


  @Bean(name = "shiroDialect")
  public ShiroDialect shiroDialect(){
    return new ShiroDialect();
  }
  
  @Bean
  public Filter CustomFormAuthenticationFilter(){
	  return new CustomFormAuthenticationFilter();
  }

}

这里注意了,在springboot下filter中不能直接直接注入service,因为 filter不能直接注入 spring容器里面的对象,我在这边的解决方法是在ShiroConfig中将应用到service的filter先实例化在引用,关键代码如下

filters.put("authc", this.CustomFormAuthenticationFilter());

@Bean
  public Filter CustomFormAuthenticationFilter(){
	  return new CustomFormAuthenticationFilter();
  }

最后要想集成成功还需配置两个文件application.yml和application-shiro.yml,其中前者关键代码如下:

 mvc:
        static-path-pattern: /**
        throw-exception-if-no-handler-found: true
        favicon:
          enabled: false
    profiles:
        active: dev
        include: shiro

后者关键代码如下:

shiro:
  loginUrl: /admin/login        # 用户未登录时跳转的请求路径
  unauthorizedUrl: /admin/login  # 用户没有访问权限时跳转的请求路径

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值