基于SpringBoot的Shiro实践应用开发总结

目录

简介

功能简介

Shiro 架构

Shiro外部来看

Shiro内部来看

Shiro工作原理

Shiro的基础知识

过滤器访问规则

URL 模式使用 Ant 风格模式 

URL 匹配顺序 

权限注解 

会话

会话监听器

项目实战

需引入的包

ShiroFilterFactoryBean的配置

Relm的登入和接口权限判断

角色权限的控制

Shiro的其他配置

记住我

基于Redis实现分布式Sesssion

防止重复登入


简介

• Apache Shiro 是 Java 的一个安全(权限)框架相对于Spring Security更加轻量
• Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在 JavaEE 环境。
• Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、 缓存等

 

功能简介

Authentication身份认证/登录,验证用户是不是拥有相应的身份;
Authorization授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普JavaSE 环境,也可以是 Web 环境的;
Cryptography加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web SupportWeb 支持,可以非常容易的集成到Web 环境;
Caching缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
• Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
• Testing:提供测试支持;
Run As允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了

 

Shiro 架构

Shiro外部来看

从外部来看Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成工作
 

Subject应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外API 核心就是 Subject。 Subject 代表了当前“用户” , 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;与 Subject 的所有交互都会委托给 SecurityManager;Subject 其实是一个门面,SecurityManager 才是实际的执行者;
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager 交互;且其管理着所有 Subject;可以看出它是 Shiro的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中DispatcherServlet 的角色
Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager 要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource

Shiro内部来看

• Subject:任何可以与应用交互的“用户”;
• SecurityManager :相当于SpringMVC 中的 DispatcherServlet;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证、授权、会话及缓存的管理。
Authenticator负责 Subject 认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authorizer授权器、 即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的 Realm;
SessionManager管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web环境,也可以用在如普通的 JavaSE 环境
CacheManager缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能
Cryptography密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密。
 

Shiro工作原理

ShiroFilterFactoryBean的静态内部类SpringShiroFilter是Spring全局的过滤器,面注册了属于shiro自己的内置过滤器。Shiro内置了几个过滤器无需手动配置,我们也可以自定义过滤器他们都会在ShiroFilterFactoryBean初始化时加载到filters里。

以下为shior默认过滤器

 SpringShiroFilter的工作流程

每个realm都有自己的数据缓存cache,二次调用直接从缓存中取数据

 

Shiro的基础知识

过滤器访问规则

• [urls] 部分的配置,其格式是: “ url=拦截器[参数],拦截器[参数]”;
• 如果当前请求的 url 匹配 [urls] 部分的某个 url 模式,将会执行其配置的拦截器。
• anon(anonymous) 拦截器表示匿名访问(即不需要登录即可访问)
• authc (authentication)拦截器表示需要身份认证通过后才能访问

URL 模式使用 Ant 风格模式 


• Ant 路径通配符支持 ?、 *、 **,注意通配符匹配不包括目录分隔符“ /”:
– ?:匹配一个字符,如 /admin? 将匹配 /admin1,但不匹配 /admin 或 /admin/;
– *:匹配零个或多个字符串,如 /admin 将匹配 /admin、/admin123,但不匹配 /admin/1;
– **:匹配路径中的零个或多个路径,如 /admin/** 将匹配 /admin/a 或 /admin/a/

URL 匹配顺序 

• URL 权限采取第一次匹配优先的方式,即从头开始使用第一个匹配的 url 模式对应的拦截器链。
• 如:
– /bb/**=filter1
– /bb/aa=filter2
– /**=filter3
– 如果请求的url是“ /bb/aa”,因为按照声明顺序进行匹配,那么将使用 filter1 进行拦截。

权限注解 

• @RequiresAuthentication:表示当前Subject已经通过login进行了身份验证;即 Subject. isAuthenticated() 返回 true
• @RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的。
• @RequiresGuest:表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
• @RequiresRoles(value={“admin”, “user”}, logical=Logical.AND):表示当前 Subject 需要角色 admin 和user
• @RequiresPermissions (value={“ user:a”, “ user:b”},logical= Logical.OR):表示当前 Subject 需要权限 user:a 或user:b

会话

Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如web容器tomcat),不管 JavaSE 还是 JavaEE 环境
都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对Web 的透明支持、 SSO 单点登录的支持等特性。

会话监听器

会话监听器用于监听会话创建、过期及停止事件

项目实战

需引入的包

  <!-- shiro  -->
  <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
  </dependency>
  <dependency>
     <groupId>org.apache.shiro</groupId>
     <artifactId>shiro-spring</artifactId>
     <version>1.4.0</version>
  </dependency>
  <dependency>
     <groupId>org.crazycake</groupId>
     <artifactId>shiro-redis</artifactId>
     <version>3.1.0</version>
  </dependency>
  <!--集成jwt实现token认证-->
  <dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>3.2.0</version>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
         <exclusions>
             <exclusion>
                  <groupId>redis.clients</groupId>
                  <artifactId>jedis</artifactId>
             </exclusion>
         </exclusions>
     <version>2.3.1.RELEASE</version>
  </dependency>
  <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
     <version>2.9.0</version>
  </dependency>

ShiroFilterFactoryBean的配置

Shiro通过Map集合组成了一个拦截器链 ,自顶向下过滤,一旦匹配,则不再执行下面的过滤。如果下面的定义与上面冲突,那按照了谁先定义谁说了算。PermissionsAuthorizationFilter继承自AuthorizationFilter所以也可以校验用户用户是否登入

  @Bean
    public ShiroFilterFactoryBean factory(SecurityManager securityManager, JdbcTemplate jdbcTemplate) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
 

        factoryBean.setSecurityManager(securityManager);
        Map<String, String> filterRuleMap = new LinkedHashMap<>();

 
        factoryBean.setLoginUrl("/litigation/system/not/logged");
        factoryBean.setUnauthorizedUrl("/litigation/system/unauthorized");
        // 配置不会被拦截的链接
        List<String> urls = getIgnoredUrlsProperties().getUrls();
        for (String url : urls) {
            filterRuleMap.put(url, "anon");
        }

        List<Map<String, Object>> menus = jdbcTemplate.queryForList("select *  from menu");
        for (Map<String, Object> menu : menus) {
            Object url = menu.get("url");
            Object perms = menu.get("perms");
            if (url != null && perms != null) {
                filterRuleMap.put(url.toString(), "perms[" + perms.toString() + "]");
            }
        }
        filterRuleMap.put("/litigation/**","authc");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;

    }
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/litigation/system")
@Slf4j
public class SystemController {

    

    @RequestMapping(value = "/not/logged")
    public String notLogged(){
        throw new ServerException(ErrorCode.NO_LOGIN);
    }

    @RequestMapping(value = "/unauthorized")
    public String unauthorized(){
        throw new ServerException(ErrorCode.NO_PERMITTED);
    }



}

 

Menu表的数据 

配置无需过滤的路径 

ignored.urls[0]=/
ignored.urls[1]=/v2/api-docs
ignored.urls[2]=/swagger**/**
ignored.urls[3]=/swagger**/**/**
ignored.urls[4]=/webjars/**
ignored.urls[5]=/v2/**
ignored.urls[6]=/litigation/system/login
ignored.urls[7]=/litigation/system/not/logged
ignored.urls[8]=/litigation/system/unauthorized
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
 * @author lzhcode
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "ignored")
public class IgnoredUrlsProperties {
    private List<String> urls = new ArrayList<>();
}

Relm的登入和接口权限判断

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;

import javax.annotation.PostConstruct;
import java.util.*;

@Slf4j
public abstract class AbstractUserRealm extends AuthorizingRealm {

    @Autowired
    private UserMapper userMapper;


    //获取用户角色的权限信息
    public abstract UserRolesAndPermissions doGetRoleAuthorizationInfo(User userInfo);

    /**
     * 获取登入用户的所有角色和权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User) principals.getPrimaryPrincipal();
        String currentLoginName = user.getMobile();
        Set<String> userRoles = new HashSet<>();
        Set<String> userPermissions = new HashSet<>();
        //从数据库中获取当前登录用户的详细信息
        Map param = new HashMap<>();
        param.put("mobile",currentLoginName);
        List<User> users = userMapper.selectListSelective(param);
        if (!CollectionUtils.isEmpty(users)) {
            User  userInfo = users.get(0);
            UserRolesAndPermissions roleContainer = doGetRoleAuthorizationInfo(userInfo);
            userRoles.addAll(roleContainer.getUserRoles());
            userPermissions.addAll(roleContainer.getUserPermissions());
        } else {
            throw new AuthorizationException();
        }
        //为当前用户设置角色和权限
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRoles(userRoles);
        authorizationInfo.addStringPermissions(userPermissions);
        return authorizationInfo;
    }

    /**
     * 登录认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken authenticationToken) throws AuthenticationException {

        //UsernamePasswordToken对象用来存放提交的登录信息
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;


        //查出是否有此用户
        Map param = new HashMap<>();
        param.put("mobile",token.getUsername());
        param.put("status",StatusEnum.NORMAL.getCode());
        List<User> users = userMapper.selectListSelective(param);
        if (!CollectionUtils.isEmpty(users)) {

            //防止重复登入
            SessionsSecurityManager securityManager = (SessionsSecurityManager) SecurityUtils.getSecurityManager();
            DefaultSessionManager sessionManager = (DefaultSessionManager) securityManager.getSessionManager();
            Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();//获取当前已登录的用户session列表
            for (Session session : sessions) {
                //清除该用户以前登录时保存的session
                //如果和当前session是同一个session,则不剔除
                if (SecurityUtils.getSubject().getSession().getId().equals(session.getId()))
                    break;
                User user = (User) (session.getAttribute("user"));
                if (user != null) {
                    String mobile = user.getMobile();
                    if (token.getUsername().equals(mobile)) {
                        log.info(mobile + "已登录,剔除中...");
                        sessionManager.getSessionDAO().delete(session);
                    }
                }
            }

            User  user = users.get(0);
            // 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验
          SimpleAuthenticationInfo  authcInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
          return authcInfo;

        }
        return null;
    }

    protected class UserRolesAndPermissions {
        Set<String> userRoles;
        Set<String> userPermissions;

        public UserRolesAndPermissions(Set<String> userRoles, Set<String> userPermissions) {
            this.userRoles = userRoles;
            this.userPermissions = userPermissions;
        }

        public Set<String> getUserRoles() {
            return userRoles;
        }

        public Set<String> getUserPermissions() {
            return userPermissions;
        }
    }

    @PostConstruct
    public void initCredentialsMatcher() {
      //该句作用是重写shiro的密码验
      setCredentialsMatcher(new CustomCredentialsMatcher());
    }
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;


@Component
public class UserRealm extends AbstractUserRealm {

    @Autowired
    private SysUserRoleMapper sysUserRoleMapper;


    @Autowired
    private SysRoleMapper sysRoleMapper;


    @Override
    public UserRolesAndPermissions doGetRoleAuthorizationInfo(User userInfo) {

        Set<String> userRoles = new HashSet<>();
        Set<String> userPermissions = new HashSet<>();
        //获取当前用户下拥有的所有角色列表,及权限
        Map param = new HashMap<>();
        param.put("userId", userInfo.getId());
        List<SysUserRole> userRoleList = sysUserRoleMapper.selectListSelective(param);
        List<String> roleIdList =  Optional.ofNullable(userRoleList).orElse(new ArrayList<>()).stream().map(temp -> {
            return temp.getRoleId().toString();
        }).collect(Collectors.toList());


        if (!CollectionUtils.isEmpty(roleIdList)) {

            List<String> rolePermissionList = sysUserRoleMapper.selectUserRolePermissionList(roleIdList);
            if (!CollectionUtils.isEmpty(rolePermissionList)) {
                for (String rolePermission : rolePermissionList) {
                    userPermissions.add(rolePermission);
                }
            }

        }

        param.clear();
        param.put("idList",roleIdList);
        List<SysRole> roleList = sysRoleMapper.selectListSelective(param);

        List<String> roleNameList =  Optional.ofNullable(roleList).orElse(new ArrayList<>()).stream().map(temp -> {
            return temp.getRole().toString();
        }).collect(Collectors.toList());

        userRoles.addAll(roleNameList);

        return new UserRolesAndPermissions(userRoles, userPermissions);
    }


}

Shiro工具类 

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.session.mgt.WebSessionKey;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class SecurityUtil {

    public static User getUser(){
        User user = (User)SecurityUtils.getSubject().getPrincipal();
        return user;
    }

    public static Session getSession(){
      return SecurityUtils.getSubject().getSession();
    }

}

 密码加密

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

public class CustomCredentialsMatcher  extends SimpleCredentialsMatcher {
  @Override
  public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info){

    UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
    Object accountCredentials = getCredentials(info);
    try {
      return PasswordHash.validatePassword(token.getPassword(), accountCredentials.toString());
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    } catch (InvalidKeySpecException e) {
      e.printStackTrace();
    }
    return false;
  }

}
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;

 
public class PasswordHash
{
    public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";

    // The following constants may be changed without breaking existing hashes.
    public static final int SALT_BYTE_SIZE = 24;
    public static final int HASH_BYTE_SIZE = 24;
    public static final int PBKDF2_ITERATIONS = 1000;

    public static final int ITERATION_INDEX = 0;
    public static final int SALT_INDEX = 1;
    public static final int PBKDF2_INDEX = 2;

    /**
     * Returns a salted PBKDF2 hash of the password.
     *
     * @param   password    the password to hash
     * @return              a salted PBKDF2 hash of the password
     */
    public static String createHash(String password)
            throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        return createHash(password.toCharArray());
    }

    /**
     * Returns a salted PBKDF2 hash of the password.
     *
     * @param   password    the password to hash
     * @return              a salted PBKDF2 hash of the password
     */
    public static String createHash(char[] password)
            throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        // Generate a random salt
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[SALT_BYTE_SIZE];
        random.nextBytes(salt);

        // Hash the password
        byte[] hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
        // format iterations:salt:hash
        return PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" +  toHex(hash);
    }

    /**
     * Validates a password using a hash.
     *
     * @param   password        the password to check
     * @param   correctHash     the hash of the valid password
     * @return                  true if the password is correct, false if not
     */
    public static boolean validatePassword(String password, String correctHash)
            throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        return validatePassword(password.toCharArray(), correctHash);
    }

    /**
     * Validates a password using a hash.
     *
     * @param   password        the password to check
     * @param   correctHash     the hash of the valid password
     * @return                  true if the password is correct, false if not
     */
    public static boolean validatePassword(char[] password, String correctHash)
            throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        // Decode the hash into its parameters
        String[] params = correctHash.split(":");
        int iterations = Integer.parseInt(params[ITERATION_INDEX]);
        byte[] salt = fromHex(params[SALT_INDEX]);
        byte[] hash = fromHex(params[PBKDF2_INDEX]);
        // Compute the hash of the provided password, using the same salt,
        // iteration count, and hash length
        byte[] testHash = pbkdf2(password, salt, iterations, hash.length);
        // Compare the hashes in constant time. The password is correct if
        // both hashes match.
        return slowEquals(hash, testHash);
    }

    /**
     * Compares two byte arrays in length-constant time. This comparison method
     * is used so that password hashes cannot be extracted from an on-line
     * system using a timing attack and then attacked off-line.
     *
     * @param   a       the first byte array
     * @param   b       the second byte array
     * @return          true if both byte arrays are the same, false if not
     */
    private static boolean slowEquals(byte[] a, byte[] b)
    {
        int diff = a.length ^ b.length;
        for(int i = 0; i < a.length && i < b.length; i++)
            diff |= a[i] ^ b[i];
        return diff == 0;
    }

    /**
     *  Computes the PBKDF2 hash of a password.
     *
     * @param   password    the password to hash.
     * @param   salt        the salt
     * @param   iterations  the iteration count (slowness factor)
     * @param   bytes       the length of the hash to compute in bytes
     * @return              the PBDKF2 hash of the password
     */
    private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes)
            throws NoSuchAlgorithmException, InvalidKeySpecException
    {
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
        return skf.generateSecret(spec).getEncoded();
    }

    /**
     * Converts a string of hexadecimal characters into a byte array.
     *
     * @param   hex         the hex string
     * @return              the hex string decoded into a byte array
     */
    private static byte[] fromHex(String hex)
    {
        byte[] binary = new byte[hex.length() / 2];
        for(int i = 0; i < binary.length; i++)
        {
            binary[i] = (byte)Integer.parseInt(hex.substring(2*i, 2*i+2), 16);
        }
        return binary;
    }

    /**
     * Converts a byte array into a hexadecimal string.
     *
     * @param   array       the byte array to convert
     * @return              a length*2 character string encoding the byte array
     */
    private static String toHex(byte[] array)
    {
        BigInteger bi = new BigInteger(1, array);
        String hex = bi.toString(16);
        int paddingLength = (array.length * 2) - hex.length();
        if(paddingLength > 0)
            return String.format("%0" + paddingLength + "d", 0) + hex;
        else
            return hex;
    }

}

涉及的主要表结构 

CREATE TABLE `user` (
  `id` int(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  `status` tinyint(4) DEFAULT '1' COMMENT '1:正常 2:停用 3锁定 -1:已删除',
  `password` varchar(255) NOT NULL DEFAULT '' COMMENT '登录密码',
  `user_name` varchar(64) DEFAULT NULL COMMENT '用户姓名',
  `mobile` varchar(20) DEFAULT NULL COMMENT '手机号',
  `latest_login_time` datetime DEFAULT NULL COMMENT '最后一次登陆时间',
  `salt` varchar(50) DEFAULT NULL COMMENT '盐',
  `create_name` varchar(50) DEFAULT NULL COMMENT '创建人',
  `update_name` varchar(50) DEFAULT NULL COMMENT '更新人',
  PRIMARY KEY (`id`),
  KEY `idx_mobile_name` (`mobile`,`user_name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='用户表';

CREATE TABLE `sys_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `role` varchar(50) DEFAULT NULL COMMENT '角色',
  `description` varchar(100) DEFAULT NULL COMMENT '角色描述',
  `available` int(11) DEFAULT '1' COMMENT '是否可用, 0-不可用,1-可用',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `modify_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `oper_name` varchar(32) DEFAULT NULL COMMENT '操作人',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_sys_role_role` (`role`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='角色表';

CREATE TABLE `sys_user_role` (
  `user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID',
  `role_id` int(11) NOT NULL DEFAULT '0' COMMENT '角色ID',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户-角色关联表';

CREATE TABLE `role_menu` (
  `role_id` int(11) NOT NULL DEFAULT '0' COMMENT '角色ID',
  `menu_id` int(11) NOT NULL DEFAULT '0' COMMENT '菜单ID',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色-菜单关联表';

CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单 ID',
  `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父类id',
  `menu_name` varchar(20) DEFAULT NULL COMMENT '菜单名称',
  `url` varchar(100) DEFAULT NULL COMMENT '菜单 URL',
  `perms` varchar(50) DEFAULT NULL COMMENT '权限标识符',
  `order_num` int(11) DEFAULT NULL COMMENT '排序',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `icon` varchar(32) DEFAULT NULL COMMENT '图标',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='菜单表';

上述两个关键方法的调用时机参考

https://blog.csdn.net/zhangjianming2018/article/details/80973548

角色权限的控制

    @RequiresRoles("admin")
    @ApiOperation(value = "角色列表接口")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", name = "token", required = true, value = "token", dataType = "String")
    })
    @PostMapping(value = "/roleList")
    public BasePageVo roleList(BasePageDto basePageDto)  {

        BasePageVo basePageVo =  roleService.roleList(basePageDto);

        return basePageVo;
    }

直接通过注解的方式和上述写在ShiroFilterFactoryBean里的拦截规则效果是一致的

Shiro的其他配置

记住我

  @Bean(name = "rememberMeCookie")
    public SimpleCookie rememberMeCookie() {
        log.info("ShiroConfiguration.rememberMeCookie()");
        // 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        // 记住我cookie生效时间30天 ,单位秒;
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
     * cookie管理对象;
     *
     * @return
     */
    @Bean(name = "rememberMeManager")
    public CookieRememberMeManager rememberMeManager() {
        log.info("ShiroConfiguration.rememberMeManager()");
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

基于Redis实现分布式Sesssion



    /**
     * SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
     * MemorySessionDAO 直接在内存中进行会话维护
     * EnterpriseCacheSessionDAO  提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
     *
     * @return
     */
    @Bean
    public SessionDAO sessionDAO(RedisConfig redisConfig) {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager(redisConfig));
        //session在redis中的保存时间,最好大于session会话超时时间
        redisSessionDAO.setExpire(12000);
        return redisSessionDAO;
    }


    /**
     * 配置保存sessionId的cookie
     * 注意:这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理 也需要自己的cookie
     * 默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid
     *
     * @return
     */
    @Bean("sessionIdCookie")
    public SimpleCookie sessionIdCookie() {
        //这个参数是cookie的名称
        SimpleCookie simpleCookie = new SimpleCookie("sid");
        //setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:

        //setcookie()的第七个参数
        //设为true后,只能通过http访问,javascript无法访问
        //防止xss读取cookie
        simpleCookie.setHttpOnly(true);
        simpleCookie.setPath("/");
        //maxAge=-1表示浏览器关闭时失效此Cookie
        simpleCookie.setMaxAge(-1);
        return simpleCookie;
    }

    /**
     * 配置会话管理器,设定会话超时及保存
     *
     * @return
     */
    @Bean("sessionManager")
    public SessionManager sessionManager(RedisConfig redisConfig) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionIdCookie(sessionIdCookie());
        sessionManager.setSessionDAO(sessionDAO(redisConfig));
        sessionManager.setCacheManager(cacheManager(redisConfig));

        //全局会话超时时间(单位毫秒),默认30分钟  暂时设置为10秒钟 用来测试
        sessionManager.setGlobalSessionTimeout(1800000);
        //是否开启删除无效的session对象  默认为true
        sessionManager.setDeleteInvalidSessions(true);
        //是否开启定时调度器进行检测过期session 默认为true
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
        //设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
        //暂时设置为 5秒 用来测试
        sessionManager.setSessionValidationInterval(3600000);
        //取消url 后面的 JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;

    }
  /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisCacheManager cacheManager(RedisConfig redisConfig) {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager(redisConfig));
        return redisCacheManager;
    }

    @Bean
    public RedisManager redisManager(RedisConfig redisConfig) {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisConfig.getRedisHost() + ":" + redisConfig.getRedisPort());
        redisManager.setPassword(redisConfig.getRedisPassword());
        redisManager.setDatabase(redisConfig.getRedisDatabase());
        redisManager.setTimeout(redisConfig.getRedisTimeOut());
        return redisManager;
    }

    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager(UserRealm userRealm, RedisConfig redisConfig) {

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置自定义 realm
        securityManager.setRealm(userRealm);
        //注入缓存管理器
        securityManager.setCacheManager(cacheManager(redisConfig));
        //如果我们需要让shiro 的 session 在集群中共享,就需要替换这个默认的 sessionManager
        securityManager.setSessionManager(sessionManager(redisConfig));
        //注入记住我管理器;
        securityManager.setRememberMeManager(rememberMeManager());
        SecurityUtils.setSecurityManager(securityManager);
        return securityManager;
    }

防止重复登入

   SessionsSecurityManager securityManager = (SessionsSecurityManager) SecurityUtils.getSecurityManager();
   DefaultSessionManager sessionManager = (DefaultSessionManager) securityManager.getSessionManager();
   Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();//获取当前已登录的用户session列表
     for (Session session : sessions) {
                //清除该用户以前登录时保存的session
                //如果和当前session是同一个session,则不剔除
           if (SecurityUtils.getSubject().getSession().getId().equals(session.getId()))
                    break;
              User user = (User) (session.getAttribute("user"));
              if (user != null) {
                 String mobile = user.getMobile();
                 if (token.getUsername().equals(mobile)) {
                        log.info(mobile + "已登录,剔除中...");
                        sessionManager.getSessionDAO().delete(session);
                }
           }
      }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据与算法架构提升之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值