springboot前后端分离使用shiro做权限管理 - Go语言中文社区

springboot前后端分离使用shiro做权限管理


最近做一个公司的小项目,使用到shiro做权限管理,在参考几位大佬的博客之后,自己也趟了无数坑,在此做一个记录。此次的springboot版本为:2.1.7.RELEASE。

话不多说,直接代码伺候:

1、shiro部分的pom文件

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <!--session持久化插件-->
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.3</version>
        </dependency>

​​​​​​​2、yml配置文件(个人比较喜欢这种写法)

server:
  port: 8080
  servlet:
    context-path: ######(你的项目路径)

netty:
  port: 8899


spring:
  datasource:
    url: jdbc:mysql://localhost:3306/##(数据库名)?useAffectedRows=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
    username: ####
    password: ####
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    initialSize: 5
    minIdle: 5

    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    http:
      multipart:
        max-file-size: 10Mb
        max-request-size: 10Mb

  redis:
    shiro:
      host: 127.0.0.1
      port: 6379
      timeout: 0
#      password: 123456


mybatis:
  mapper-locations: classpath:mybatis/mapper/*.xml


pagehelper:
  helperDialect: mysql
  reasonable: true #开启优化,<1返回第一页
  supportMethodsArguments: true #是否支持接口参数来传递分页参数,默认false
  pageSizeZero: false #pageSize=0 返回所有
  params: count=countSql

3、shiro的相关配置类

(1)、关于跨域配置的问题,因为项目是前后端完全分离,不可避免会涉及到跨域问题。很奇怪的是,我明明已经配置了跨域,但是在实际运行的时候,还是会出问题,所以添加针对shiro的跨域配置。

/**
 * @ClassName ShiroCrosConfig
 * @Description shiro跨域问题配置
 * @Author Innocence
 **/
@Configuration
public class ShiroCrosConfig extends BasicHttpAuthenticationFilter {

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); //标识允许哪个域到请求,直接修改成请求头的域
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");//标识允许的请求方法
        // 响应首部 Access-Control-Allow-Headers 用于 preflight request (预检请求)中,列出了将会在正式请求的 Access-Control-Expose-Headers 字段中出现的首部信息。修改为请求首部
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        //给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

(2)、关于sessionId获取的方法,我们在使用shiro时,用户认证成功之后会给前端返回一个token,这个token一般是shiro的sessionid。之后前端的每个请求都要带上这个token用于校验。

/**
 * @ClassName MySessionManager
 * @Description 自定义的session获取
 * @Author Innocence
 **/
public class MySessionManager extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "Authorization";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public MySessionManager(){
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response){
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果请求头中有AUTHORIZATION,其值为sessionid
        if (!StringUtils.isBlank(id)){
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        }
        //否则按默认规则从cookie取sessionId
        return super.getSessionId(request, response);

    }
}

(3)、其它问题,这个问题当时处理之后忘记记录了。。。。尴尬

@Configuration
public class MyFormAuthorizationFilter extends FormAuthenticationFilter {
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(servletRequest);
        if ("OPTIONS".equals(httpServletRequest.getMethod())) {
            return true;
        }
        return super.isAccessAllowed(servletRequest, servletResponse, o);
    }

}

(4)、重头戏来了!!!shiro的核心配置,相关解释在代码注释

@Configuration
public class ShiroConfig {

    @Value("${spring.redis.shiro.host}")
    private String host;
    @Value("${spring.redis.shiro.port}")
    private int port;
    @Value("${spring.redis.shiro.timeout}")
    private int timeout;
//    @Value("${spring.redis.shiro.password}")
//    private String password;

    private Logger logger = LoggerFactory.getLogger(this.getClass());
    /*
    * 创建ShrioFilterFactoryBean
    * @author Innocence
    * @date 2019/9/25 002510:57
    */
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
        Map<String, String> filterMap = new LinkedHashMap<>();
        /*
        * Shiro内置过滤器,可以实现权限相关的拦截器,常用的有:
        *   anon:无需认证(登录)即可访问
        *   authc:必须认证才可以访问
        *   user:如果使用rememberme的功能可以直接访问
        *   perms:该资源必须得到资源权限才能访问
        *   role:该资源必须得到角色资源才能访问
        */
//        filterMap.put("/admin/**", "authc,roles[超级管理员]");
        //放过登录请求
        filterMap.put("/user/login", "anon");
//       放过文件上传的接口请求
        filterMap.put("/goods/uploadFile","anon");
//        filterMap.put("/admin/deleteOne","authc,roles[超级管理员,店长]");
//        filterMap.put("/user/getInitMenus","authc");
        //放过swagger2
        filterMap.put("/swagger-ui.html","anon");
        filterMap.put("/swagger-resources", "anon");
        filterMap.put("/swagger-resources/configuration/security", "anon");
        filterMap.put("/swagger-resources/configuration/ui", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/webjars/springfox-swagger-ui/**", "anon");

        filterMap.put("/**",  "authc");//其他资源全部拦截
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

//        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
//        配置shiro默认登录地址,前后端分离应该由前端控制页面跳转
        shiroFilterFactoryBean.setLoginUrl("/unauth");
        return shiroFilterFactoryBean;
    }
    /*
     * 创建DefaultWebSecurityManager
     * @author Innocence
     * //SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
     * @date 2019/9/25 002510:57
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //注入rememberme对象
        securityManager.setRememberMeManager(cookieRememberMeManager());
        //注入session
        securityManager.setSessionManager(sessionManager());
//        //注入缓存管理对象
        securityManager.setCacheManager(redisCacheManager());
        //将Realm注入SecurityManager
        securityManager.setRealm(userRealm());
        return securityManager;
    }
    /*
    * 创建Realm
    * @author Innocence
    * @date 2019/9/25 002510:59
    */
    @Bean(name = "userRealm")
    public UserRealm userRealm(){
        UserRealm userRealm = new UserRealm();
        //设置解密规则
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        userRealm.setCachingEnabled(false);
        return userRealm;
    }

    /**
    * 因为密码是加过密的,所以,如果要Shiro验证用户身份的话,需要告诉它我们用的是md5加密的,并且是加密了两次。
     * 同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。
     * 这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
    * @author Innocence
    * @date 2019/9/27 002716:12
    * @return org.apache.shiro.authc.credential.HashedCredentialsMatcher
    */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //使用MD5散列算法
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //散列两次,相当于MD5(MD5(“”))
        hashedCredentialsMatcher.setHashIterations(2);
        // storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /*
     * z自定义sessionmanager
     * @author Innocence
     * @date 2019/10/17 001710:02
     * @param []
     * @return org.apache.shiro.session.mgt.SessionManager
     */
    @Bean(name = "sessionManager")
    public SessionManager sessionManager(){
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setSessionIdUrlRewritingEnabled(false); //取消登陆跳转URL后面的jsessionid参数
        mySessionManager.setSessionDAO(redisSessionDAO());
        mySessionManager.setGlobalSessionTimeout(-1);//不过期
        return mySessionManager;
    }

    /*
     * 配置shiro redisManager
     * 使用shiro-redis开源插件
     * @author Innocence
     * @date 2019/10/17 001710:03
     * @param []
     * @return org.crazycake.shiro.RedisManager
     */
    @Bean(name = "redisManager")
    public RedisManager redisManager(){
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host+":"+port);
        redisManager.setTimeout(timeout);
//        redisManager.setPassword(password);
        return redisManager;
    }

    /*
     * cacheManager的Redis实现
     * @author Innocence
     * @date 2019/10/17 001710:20
     * @param []
     * @return org.crazycake.shiro.RedisCacheManager
     */
    @Bean(name = "redisCacheManager")
    public RedisCacheManager redisCacheManager(){
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /*
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用shiro-redis开源插件
     * @author Innocence
     * @date 2019/10/17 001710:23
     * @param []
     * @return org.crazycake.shiro.RedisSessionDAO
     */
    @Bean(name = "redisSessionDAO")
    public RedisSessionDAO redisSessionDAO(){
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }
    /*
    * 开启shiro 的aop注解支持
    * @author Innocence
    * @date 2019/9/27 002716:22
    * @param [securityManager]
    * @return org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
    */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /*
    * cookie对象
    * @author Innocence
    * @date 2019/9/27 002716:24
    * @return org.apache.shiro.web.servlet.SimpleCookie
    */
    @Bean
    public SimpleCookie rememberMeCookie() {
        logger.info("ShiroConfiguration.rememberMeCookie()=============");
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //<!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /*
    * cookie管理对象
    * @author Innocence
    * @date 2019/9/27 002716:24
    * @return org.apache.shiro.web.mgt.CookieRememberMeManager
    */
    @Bean
    public CookieRememberMeManager cookieRememberMeManager() {
        logger.info("ShiroConfiguration.rememberMeManager()========");
        CookieRememberMeManager manager = new CookieRememberMeManager();
        manager.setCookie(rememberMeCookie());
        return manager;
    }


    /*
    * 加入下面两个bean,shiro才会执行授权逻辑
    * @author Innocence
    * @date 2019/11/6 000616:15
    * @param []
    * @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
    */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

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

附上针对shiro的MD5密码加密方法

public String  encodePassword(String userName,String passWord){
        String hashAlgorithName = "MD5";
        int hashIterations = 2;
        String password =passWord;
        ByteSource credentialsSalt = ByteSource.Util.bytes(userName+"salt");
        String obj =String.valueOf(new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations));
        return obj;
    }

4、shiro的自定义realm

public class UserRealm extends AuthorizingRealm {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private PermissionMapper permissionMapper;

    /*
    * 执行授权逻辑:因为设计每个用户只有一个角色,所以roleMapper.getRoleByUserName(user)返回值只有一个实体。
    * @author Innocence
    */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("自定义UserRealm执行授权逻辑开始==================");
        User user =(User) principalCollection.getPrimaryPrincipal();
        if (null != user){
            List<String> roleLists = new ArrayList<>();
            List<String> permissionLists = new ArrayList<>();
            Role role = roleMapper.getRoleByUserName(user);
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            roleLists.add(role.getRoleName());
            info.addRoles(roleLists);
            List<Permission> permissionByRoleId = permissionMapper.getPermissionByRoleId(role);
            for (Permission permission:permissionByRoleId) {
                permissionLists.add(permission.getPermission());
                info.addStringPermissions(permissionLists);
            }
            return info;
        }
        return null;
    }

    /*
    * 执行认证逻辑
    * @author Innocence
    */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        logger.info("自定义UserRealm执行认证逻辑开始==================");
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        String username = token.getUsername();
        char[] password = token.getPassword();
        String pass = String.valueOf(password);
        User user = new User();
        user.setUserName(username);
        user.setUserPassWord(pass);
        User userInfo = userMapper.getOneUserInfo(user);
        if (null == userInfo){
            //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                //这里的第一个参数,可以是查询到的用户实体,也可以是用户名。主要是为方便后期Subject的getPrincipal()方法取值。放进去是什么,getPrincipal()取到的就是什么。
                userInfo,
                //这里的密码,一定是查询到的实体密码,不是参数传递的密码
                userInfo.getUserPassWord(),
                ByteSource.Util.bytes(userInfo.getUserName()+"salt"),//salt=userName+salt
                getName()
        );
        return authenticationInfo;
    }

至此,springboot前后端分离集成shiro基本完成,附上几张经典权限表:

1、用户表

2、角色表

 3、权限表

 4、用户角色中间表

5、角色权限中间表

接下来,测试用户登录

@RequestMapping("/login")
    public Result userLogin(User user){
        Result res = new Result();
        logger.info("用户登陆请求到达");
        // 登录失败从request中获取shiro处理的异常信息。
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getUserPassWord());
        Subject subject = SecurityUtils.getSubject();
        try{
            subject.login(token);
            //从UserRealm里返回的SimpleAuthenticationInfo获取到认证成功的用户名,
            //subject.getPrincipal()获取的是SimpleAuthenticationInfo设置的第一个参数
            User loginUser = (User) subject.getPrincipal();
            Session session = subject.getSession();
            session.setAttribute("loginUser" ,loginUser);
//            SessionCache.sessionCache.put((String)subject.getSession().getId(),session);
            loginUser.setUserPassWord("");
            res.put("loginUser",loginUser);
            res.put("token",subject.getSession().getId());
            res.put("code",ResultCommon.SUCCESS_CODE);
        }catch (IncorrectCredentialsException e){
            res.put("code",ResultCommon.FAILED_CODE);
            res.put("msg","密码错误");
        }catch (LockedAccountException e){
            res.put("code",ResultCommon.FAILED_CODE);
            res.put("msg","账户被冻结");
        }catch (AuthenticationException e) {
            res.put("code",ResultCommon.FAILED_CODE);
            res.put("msg","账户不存在");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return res;
    }

 搞定收工!

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_43753812/article/details/102967686
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-04-19 13:29:22
  • 阅读 ( 1419 )
  • 分类:研发管理

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢