shiro1

本人代码https://github.com/renxiwen/shiro-learn,仅供参考。

#一,引入shiro框架
*环境:所以将shiro的一些经验记录下来,希望能帮助到读到本文的你,本文环境shiro1.3.2、spring4.3.5没有使用boot、jdk8、tomcat8.5、mysql5.6、redis3.2.11。
目标:通过shiro实现对后端接口进行权限控制,所有静态资源都不需要权限验证,使用redis实现session共享,避免session单点问题。
shiro版本:1.4.1
引入需要的jar文件

		<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.1</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.1</version>
        </dependency>
  • web.xml中增加spring的代理filter,实际filter是shiro的ShiroFilterFactoryBean(后面说ShiroFilterFactoryBean的具体配置),targetFilterLifecycle为true代表由spring控制该filter的生命周期.
  • 一般这段配置不需要改,只需要改url-pattern。shiro源码中的sample配的是/*,因为我只想控制用户访问后端接口的权限,静态资源可以随便访问,所以后端接口都用的api开头,让shiro过滤器只对api开头的请求生效(这样还可以避免频繁访问session,造成redis压力过大的问题,具体后面说)
	<filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/api/*</url-pattern>
    </filter-mapping>
  • 在spring的配置文件中配置ShiroFilterFactoryBean,注意id一定要和web.xml中 的filter-name一致,否则proxyFilter找不到实际filter。
  • shiro的内置过滤器可以百度每个的含义和配制方法,这里需要说下authc。使用authc的接口是一定要输入用户名密码登陆后才能访问,哪怕通过shiro的rememberMe自动登录的用户也需要重新输入用户名密码重新登录后才能访问。
	<!--配置全局权限过滤器-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!--shiro最核心组件,具体后面说-->
        <property name="securityManager" ref="securityManager"/>
        <!--设置没有登录时的跳转地址-->
        <property name="loginUrl" value="/static/html/login.html"/>
        <!--设置权限不足时的跳转地址-->
        <property name="unauthorizedUrl" value="/static/html/noPerm.html"/>
        <!--对哪个后端接口使用哪个过滤器进行配置,等号后边是shiro内置过滤器的名字-->
        <property name="filterChainDefinitions">
            <value>
                <!--匿名访问,/api/login是登陆接口,当然可以随便访问-->
                /api/login = anon
                /api/test? = authc
                <!--本工程中上传文件的接口,只允许有common角色的用户访问-->
                /api/file = roles[common]
                <!--用户退出登录的接口,后端不需要实现该接口,logout拦截到/api/logout的url后,就自动清除登录状态回到首页了-->
                <!--因为在web.xml中设置的url-parttern是/api/*,随意只有api开头的url才会被拦截-->
                /api/logout = logout
                <!--使用自定义拦截器的接口-->
                <!--/api/selfFilter = myFilter-->
                <!--其他所有接口都需要认证,也就是需要之前输入过账号密码登录过-->
                /** = authc
            </value>
        </property>
        <!--加入自定义filter,在filterChainDefinitions可以通过key来引用-->
        <!--<property name="filters">-->
            <!--<map>-->
                <!--<entry key="myFilter">-->
                    <!--<bean class="MyFilter"/>-->
                <!--</entry>-->
            <!--</map>-->
        <!--</property>-->
    </bean>

二配置并自定义realm

shiro中realm的是进行认证和授权的组件,自带了几种实现,比如jdbcRealm和iniRealm,实际项目中肯定都是自己实现realm

首先需要建立用户表,存放用户名、密码、权限信息

	CREATE TABLE IF NOT EXISTS `user` (
	  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
	  `user_name` varchar(10) NOT NULL DEFAULT '' COMMENT '用户名',
	  `password` varchar(50) NOT NULL DEFAULT '' COMMENT '用户密码,用户名为盐,五次md5',
	  `roles` varchar(20) NOT NULL DEFAULT '' COMMENT '角色名,逗号分隔',
	  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
	  PRIMARY KEY (`id`),
	  UNIQUE KEY `uniq_idx_role_name` (`user_name`)
	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

实现dao增删改查(使用的mybatis代理dao),封一个service实际操作user表

	@Service
	public class UserService {
	    //mybatis代理实现的dao
	    @Resource
	    private UserDao userDao;
	
	    //根据用户名获得user对象
	    public User queryUserByName(String name) {
	        try {
	            if (StringUtils.isBlank(name)) {
	                return null;
	            }
	            return userDao.queryUserByName(name);
	        } catch (Exception e) {
	            log.error("db error when query user:{}", name, e);
	        }
	        return null;
	    }
	    
	    //根据用户名获得用户的所有角色
	    public Set<String> queryUserRole(String userName) {
	        User user = queryUserByName(userName);
	        if (user == null) {
	            return Collections.emptySet();
	        }
	        List<String> roleList = StringAssist.splitComma(user.getRoles());
	        return Sets.newHashSet(roleList);
	    }
	}

定义登录的接口

	@PostMapping("login")
    public String login(String username, String password) {
        try {
            //shiro通过SecurityUtils.getSubject()获得主体,主体可以理解为客户端实例,原理在后面讲
            Subject subject = SecurityUtils.getSubject();
            //已经认证过,也就是该客户端已经登陆过
            if (subject.isAuthenticated()) {
                return "redirect:/static/html/indexLogin.html";
            }
            //一般都使用UsernamePasswordToken,shiro的token中有Principal和Credentials的概念
            //Principal代表当前客户端要登录的用户,Credentials代表证明该用户身份的凭证
            //UsernamePasswordToken将username作为Principal,password作为Credentials
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            //rememberMe功能后面讲
            token.setRememberMe(true);
            subject.login(token);
        } catch (AuthenticationException e) {
            //登录失败则跳转到登录失败页面,可能是用户名或密码错误
            return "redirect:/static/html/loginError.html";
        }
        return "redirect:/static/html/indexLogin.html";
    }

自定义MyRealm继承AuthorizingRealm,分别实现认证和授权的方法

doGetAuthenticationInfo是认证的方法,当用户登陆的时候会调用;
doGetAuthorizationInfo是授权的方法,在拦截器中进行权限校验的时候会调用;

public class MyRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    //用户的权限信息包含roles角色和permission权限两部分,我这里只使用了角色进行进行权限控制
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //principals.getPrimaryPrincipal()获得的就是当前用户名
        if (principals == null || StringUtils.isBlank((String) principals.getPrimaryPrincipal())) {
            return null;
        }
        //将用户角色信息传入SimpleAuthorizationInfo
        return new SimpleAuthorizationInfo(userService.queryUserRole((String) principals.getPrimaryPrincipal()));
    }

    //token实际就是在login时传入的UsernamePasswordToken
    //getPrincipal()中只执行了getUsername(),getCredentials()只执行了getPassword()
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        if (token == null||StringUtils.isBlank((String) token.getPrincipal())) {
            return null;
        }
        //根据token中的用户名查库,获得user对象
        User user = userService.queryUserByName((String) token.getPrincipal());
        if (user == null) {
            return null;
        }
        //SimpleAuthenticationInfo代表该用户的认证信息,其实就是数据库中的用户名、密码、加密密码使用的盐
        //存在数据库中的密码是对用户真是密码通过md5加盐加密得到的,保证安全,及时数据泄露,也得不到真正的用户密码
        //getName()返回该realm的名字,代表该认证信息的来源是该realm,作用不大,一般都是单realm
        //该方法返回后,上层会对token和SimpleAuthenticationInfo进行比较,首先比较Principal(),然后将token的Credentials
        //进行md5加上SimpleAuthenticationInfo中的盐加密,加密结果和SimpleAuthenticationInfo的Credentials比较
        return new SimpleAuthenticationInfo(
                user.getUserName(), user.getPassword(), ByteSource.Util.bytes(user.getUserName()), getName());
    }
}

在securityManager中注入realm,其中authenticator必须先于realms注入,这一点非常关键,我之前无论如何都无法授权,debug发现authenticator中的realms为空

	<!--非web环境使用DefaultSecurityManager-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!--多realm的授权策略设置,配置为必须满足全部realm才算成功,不在realms前配置的话authenticator中的realms集合为空-->
        <!--securityManager注入realms的时候,会把realm也放一份到authenticator中,所以必须写在realms上面!!!-->
        <property name="authenticator">
            <bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
                <property name="authenticationStrategy">
                    <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/>
                </property>
            </bean>
        </property>
        <!--如果只有一个realm的话,可以直接注入realm属性,不需要注入realms属性-->
        <!--为了以后的扩展,即使只有一个realm还是注入了realms属性(虽然以后估计也都是单realm)-->
        <property name="realms">
            <list>
                <bean class="com.qunar.lfz.shiro.MyRealm">
                    <property name="credentialsMatcher">
                        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                            <!--指定加密算法-->
                            <property name="hashAlgorithmName" value="MD5"/>
                            <!--指定对密码连续进行5轮md5加密-->
                            <property name="hashIterations" value="5"/>
                        </bean>
                    </property>
                </bean>
            </list>
        </property>
    </bean>

因为我们指定了用户的原密码通过5次md5加盐加密进行校验,这也就要求用户注册的时候存入数据库的密码也是经过5次md5加盐加密的。shiro提供了Md5Hash工具类,通过new Md5Hash(“原密码”, “盐值”, 5).toString()查看加密后的密码;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值