shiro入门实战

​​​​​​​Apache Shiro | Simple. Java. Security.

java语言编写

架构

shiro入门实战_第1张图片

 shiro认证流程

shiro入门实战_第2张图片

使用

添加shiro依赖

        
            org.apache.shiro
            shiro-core
            1.4.0
        

SimpleAccountRealm

SimpleAccountRealm只支持role的授权 hasRole、checkRole 

授权是认证之后的操作 

public void authen() {
        //认证的发起者(subject),   SecurityManager,   Realm
        //1. 准备Realm(基于内存存储用户信息)
        SimpleAccountRealm realm = new SimpleAccountRealm();
        realm.addAccount("admin", "admin", "超级管理员", "商家");

        //2. 准备SecurityManager
        DefaultSecurityManager securityManager = new DefaultSecurityManager();

        //3. SecurityManager和Realm建立连接
        securityManager.setRealm(realm);

        //4. subject和SecurityManager建立联系
        SecurityUtils.setSecurityManager(securityManager);

        //5. 声明subject
        Subject subject = SecurityUtils.getSubject();

        //6. 发起认证
        subject.login(new UsernamePasswordToken("admin", "admin"));
        // 如果认证时,用户名错误,抛出:org.apache.shiro.authc.UnknownAccountException异常
        // 如果认证时,密码错误,抛出:org.apache.shiro.authc.IncorrectCredentialsException:

        //7. 判断是否认证成功
        System.out.println(subject.isAuthenticated());

        //8. 退出登录后再判断
        //        subject.logout();
        //        System.out.println("logout方法执行后,认证的状态:" + subject.isAuthenticated());

        //9. 授权是在认证成功之后的操作!!!
        // SimpleAccountRealm只支持角色的授权
        System.out.println("是否拥有超级管理员角色:" + subject.hasRole("超级管理员"));
        subject.checkRole("商家");
        // check方法校验角色时,如果没有指定角色,会抛出异常:org.apache.shiro.authz.UnauthorizedException: Subject does not have role [角色信息]
    }

IniRealm

基于文件存储用户名,密码,角色等信息

支持权限校验

public void authen(){
        //1. 构建IniRealm
        IniRealm realm = new IniRealm("classpath:shiro.ini");

        //2. 构建SecurityManager绑定Realm
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        securityManager.setRealm(realm);

        //3. 基于SecurityUtils绑定SecurityManager并声明subject
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();

        //4. 认证操作
        subject.login(new UsernamePasswordToken("admin","admin"));

        //5. 角色校验
        // 超级管理员
        System.out.println(subject.hasRole("超级管理员"));
        subject.checkRole("运营");

        //6. 权限校验
        System.out.println(subject.isPermitted("user:update"));
        // 如果没有响应的权限,就抛出异常:UnauthorizedException: Subject does not have permission [user:select]
        subject.checkPermission("user:delete");
    }

shiro.ini

[users]
username=password,role1,role2
admin=admin,超级管理员,运营
[roles]
role1=perm1,perm2
超级管理员=user:add,user:update,user:delete

JdbcRealm

通过数据库存储对应的用户、角色、权限信息

推荐使用经典五张表来存储

shiro入门实战_第3张图片

    public void authen(){
        //1. 构建JdbcRealm
        JdbcRealm realm = new JdbcRealm();

        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql:///shiro");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        realm.setDataSource(dataSource);

        // 开启权限校验
        realm.setPermissionsLookupEnabled(true);

        //2. 构建SecurityManager绑定Realm
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        securityManager.setRealm(realm);

        //3. 基于SecurityUtils绑定SecurityManager并声明subject
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();

        //4. 认证操作
        subject.login(new UsernamePasswordToken("admin","admin"));

        //5. 授权操作(角色)
        System.out.println(subject.hasRole("超级管1理员"));

        //6. 授权操作(权限)
        System.out.println(subject.isPermitted("user:add"));

    }

jdbcRealm默认不支持权限校验,需要手动开启setPermissionLookupEnabled(true)

表需要按照它内部的结构来进行定义,需要表结构不一致,也可以使用自定义的校验sql

CustomRealm(自定义)推荐

需要手动创建CustomRealm,并且继承AuthorizingRealm ,

认证

重写doGetAuthenticationInfo方法完成自定义Realm认证

public class CustomRealm extends AuthorizingRealm {


    /**
     * 认证方法,只需要完成用户名校验即可,密码校验由Shiro内部完成
     * @param token  用户传入的用户名和密码
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1. 基于Token获取用户名
        String username = (String) token.getPrincipal();

        //2. 判断用户名(非空)
        if(StringUtils.isEmpty(username)){
            // 返回null,会默认抛出一个异常,org.apache.shiro.authc.UnknownAccountException
            return null;
        }

        //3. 如果用户名不为null,基于用户名查询用户信息
        User user = this.findUserByUsername(username);

        //4. 判断user对象是否为null
        if(user == null){
            return null;
        }

        //5. 声明AuthenticationInfo对象,并填充用户信息
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),"CustomRealm!!");
   
        //6. 返回info
        return info;
    }

校验同其他real基本相同

认证(密码加密加盐)

虽然MD5加密不可逆,但是又一些网站可以把大量常用的密码加密后的结果存储起来,这样MD5的加密也可能会被破解

密码存储的时候需要加密加盐,还需要把对应的salt存储起来,认证时需要拿到对应的salt进行加密然后比较

public class CustomRealm extends AuthorizingRealm {

    {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");
        matcher.setHashIterations(1024);
        this.setCredentialsMatcher(matcher);
    }


    /**
     * 认证方法,只需要完成用户名校验即可,密码校验由Shiro内部完成
     * @param token  用户传入的用户名和密码
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1. 基于Token获取用户名
        String username = (String) token.getPrincipal();

        //2. 判断用户名(非空)
        if(StringUtils.isEmpty(username)){
            // 返回null,会默认抛出一个异常,org.apache.shiro.authc.UnknownAccountException
            return null;
        }

        //3. 如果用户名不为null,基于用户名查询用户信息
        User user = this.findUserByUsername(username);

        //4. 判断user对象是否为null
        if(user == null){
            return null;
        }

        //5. 声明AuthenticationInfo对象,并填充用户信息
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),"CustomRealm!!");
        // 设置盐!
        info.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));
        //6. 返回info
        return info;
    }

授权

授权是在认证之后的操作,授权操作需要重写doGetAuthorizationInfo方法

    // 授权方法,授权是在认证之后的操作
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1. 获取认证用户的信息
        User user = (User) principals.getPrimaryPrincipal();

        //2. 基于用户信息获取当前用户拥有的角色。
        Set roleSet = this.findRolesByUser();

        //3. 基于用户拥有的角色查询权限信息
        Set permSet = this.findPermsByRoleSet(roleSet);

        //4. 声明AuthorizationInfo对象作为返回值,传入角色信息和权限信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roleSet);
        info.setStringPermissions(permSet);

        //5. 返回
        return info;
    }

 shiro整合web的流程

shiro入门实战_第4张图片

shiro不太适合前后端分离的项目,前后端分离的项目,推荐使用JWT

shiro整合springboot

pom

    
        
            org.springframework.boot
            spring-boot-starter-data-redis
        

        
            org.springframework.boot
            spring-boot-starter-web
        

        
            mysql
            mysql-connector-java
            5.1.47
        

        
            com.alibaba
            druid-spring-boot-starter
            1.1.10
        

        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.2.2
        

        
            org.apache.shiro
            shiro-spring-boot-web-starter
            1.4.0
        

    

application.yml

shiro:
  loginUrl: /login.html
  unauthorizedUrl: /401.html   # 针对过滤器链生效,针对注解是不生效的

配置类

@Configuration
public class ShiroConfig {

    @Bean
    public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
        DefaultRedisWebSessionManager sessionManager = new DefaultRedisWebSessionManager();
        sessionManager.setSessionDAO(sessionDAO);
        return sessionManager;
    }



    @Bean
    public DefaultWebSecurityManager securityManager(ShiroRealm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setSessionManager(sessionManager);
        // 设置CacheManager,提供与Redis交互的Cache对象
        securityManager.setCacheManager(redisCacheManager);
        return securityManager;
    }

    @Bean
    public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
        DefaultShiroFilterChainDefinition shiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();

        Map filterChainDefinitionMap = new LinkedHashMap();
        filterChainDefinitionMap.put("/login.html","anon");
        filterChainDefinitionMap.put("/user/logout","logout");
        filterChainDefinitionMap.put("/user/**","anon");
        filterChainDefinitionMap.put("/item/rememberMe","user");
        filterChainDefinitionMap.put("/item/authentication","authc");
        filterChainDefinitionMap.put("/item/select","rolesOr[超级管理员,运营]");
        filterChainDefinitionMap.put("/item/delete","perms[item:delete,item:insert]");
        filterChainDefinitionMap.put("/**","authc");

        shiroFilterChainDefinition.addPathDefinitions(filterChainDefinitionMap);

        return shiroFilterChainDefinition;
    }

    @Value("#{ @environment['shiro.loginUrl'] ?: '/login.jsp' }")
    protected String loginUrl;

    @Value("#{ @environment['shiro.successUrl'] ?: '/' }")
    protected String successUrl;

    @Value("#{ @environment['shiro.unauthorizedUrl'] ?: null }")
    protected String unauthorizedUrl;


    @Bean
    protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {

        //1. 构建ShiroFilterFactoryBean工厂
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();

        //2. 设置了大量的路径
        filterFactoryBean.setLoginUrl(loginUrl);
        filterFactoryBean.setSuccessUrl(successUrl);
        filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);

        //3. 设置安全管理器
        filterFactoryBean.setSecurityManager(securityManager);

        //4. 设置过滤器链
        filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());

        //5. 设置自定义过滤器 , 这里一定要手动的new出来这个自定义过滤器,如果使用Spring管理自定义过滤器,会造成无法获取到Subject
        filterFactoryBean.getFilters().put("rolesOr",new RolesOrAuthorizationFilter());



        //6. 返回工厂
        return filterFactoryBean;
    }


}

shiro的过滤器

shiro入门实战_第5张图片

 角色校验使用roles

权限校验使用perms

自定义过滤器

写自定义过滤器

public class RolesOrAuthorizationFilter extends AuthorizationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        // 获取主体subject
        Subject subject = getSubject(request, response);
        // 将传入的角色转成数组操作
        String[] rolesArray = (String[]) mappedValue;
        // 健壮性校验
        if (rolesArray == null || rolesArray.length == 0) {
            return true;
        }
        // 开始校验
        for (String role : rolesArray) {
            if(subject.hasRole(role)){
                return true;
            }
        }

        return false;
    }
}

将自定义过滤器配置给shiro

shiro配置文件中将自定义过滤器配置进去

    @Bean
    protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {

        //1. 构建ShiroFilterFactoryBean工厂
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();

        //2. 设置了大量的路径
        filterFactoryBean.setLoginUrl(loginUrl);
        filterFactoryBean.setSuccessUrl(successUrl);
        filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);

        //3. 设置安全管理器
        filterFactoryBean.setSecurityManager(securityManager);

        //4. 设置过滤器链
        filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());

        //5. 设置自定义过滤器 , 这里一定要手动的new出来这个自定义过滤器,如果使用Spring管理自定义过滤器,会造成无法获取到Subject
        filterFactoryBean.getFilters().put("rolesOr",new RolesOrAuthorizationFilter());



        //6. 返回工厂
        return filterFactoryBean;
    }

加了@Bean注解的方法的参数值也都是从spring容器中获取

springboot项目中,默认有一个过滤器

shiro入门实战_第6张图片

使用注解授权

@RequiresRoles(value={"role1","role2"}) 

注解进行授权时,是基于对Controller类进行代理,在前置增强中对请求进行权限校验

shiro入门实战_第7张图片

 在SpringBoot中注解默认就生效,是因为自动装配中,已经配置好了对注解的支持

shiro入门实战_第8张图片

注解的形式无法将错误页面的信息定位到401.html,因为配置的这种路径,只针对过滤器链有效,注解无效。为了实现友好提示的效果,可以配置异常处理器,@RestControllerAdvice,@ControllerAdvice  

记住我,remember me

springboot自动装配

shiro入门实战_第9张图片

 rememberMe是基于user过滤器实现的,适用于安全等级较低的页面

只要登陆过,不需要再次登录

认证登录时,添加rememberMe

UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(rememberMe != null && "on".equals(rememberMe));
subject.login(token);

认证后,需要以浏览器的cookie和后台的user对象绑定,进行持久化,所以需要user序列化(实现Serializable)

需要在realm授权方法前重新鉴权(因为cookie绑定的是认证成功后,返回的第一个参数,而第一个参数和授权方法中参数能获得到的用户信息是一个内容。直接在授权方法中先做认证判断 )

Shiro在认证成功后,可以不依赖Web容器的Session,也可以依赖!

在SpringBoot自动装配之后,Shiro默认将HttpSession作为存储用户认证成功信息的位置。

但是SpringBoot也提供了一个基于JVM内存(HashMap)存储用户认证信息的位置。

使用springboot提供的MemorySession来存储用户认证信息:

修改Shiro默认使用的SessionDAO,修改为默认构建好的MemorySessionDAO

// 构建管理SessionDAO的SessionManager
@Bean
public SessionManager sessionManager(SessionDAO sessionDAO) {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionDAO(sessionDAO);
    return sessionManager;
}

@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm,SessionManager sessionManager){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(realm);
	// 将使用MemorySessionDAO的SessionManager注入到SecurityManager
    securityManager.setSessionManager(sessionManager);
    return securityManager;
}

将认证信息存储在redis中,可以实现分布式系统的功能

重写SessionDAO (extends AbstractSessionDAO),实现redis的相关操作

@Component
public class RedisSessionDAO extends AbstractSessionDAO {

    @Resource
    private RedisTemplate redisTemplate;

    // 存储到Redis时,sessionId作为key,Session作为Value
    // sessionId就是一个字符串
    // Session可以和sessionId绑定到一起,绑定之后,可以基于Session拿到sessionId
    // 需要给Key设置一个统一的前缀,这样才可以方便通过keys命令查看到所有关联的信息

    private final String SHIOR_SESSION = "session:";

    @Override
    protected Serializable doCreate(Session session) {
        System.out.println("Redis---doCreate");
        //1. 基于Session生成一个sessionId(唯一标识)
        Serializable sessionId = generateSessionId(session);

        //2. 将Session和sessionId绑定到一起(可以基于Session拿到sessionId)
        assignSessionId(session, sessionId);

        //3. 将 前缀:sessionId 作为key,session作为value存储
        redisTemplate.opsForValue().set(SHIOR_SESSION + sessionId,session,30, TimeUnit.MINUTES);

        //4. 返回sessionId
        return sessionId;
    }

 	@Override
    protected Session doReadSession(Serializable sessionId) {
        //1. 基于sessionId获取Session (与Redis交互)
        if (sessionId == null) {
            return null;
        }
        Session session = (Session) redisTemplate.opsForValue().get(SHIOR_SESSION + sessionId);
        if (session != null) {
            redisTemplate.expire(SHIOR_SESSION + sessionId,30,TimeUnit.MINUTES);
        }
        return session;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        System.out.println("Redis---update");
        //1. 修改Redis中session
        if(session == null){
            return ;
        }
        redisTemplate.opsForValue().set(SHIOR_SESSION + session.getId(),session,30, TimeUnit.MINUTES);
    }

    @Override
    public void delete(Session session) {
        // 删除Redis中的Session
        if(session == null){
            return ;
        }
        redisTemplate.delete(SHIOR_SESSION + session.getId());
    }

    @Override
    public Collection getActiveSessions() {
        Set keys = redisTemplate.keys(SHIOR_SESSION + "*");

        Set sessionSet = new HashSet<>();
        // 尝试修改为管道操作,pipeline(Redis的知识)
        for (Object key : keys) {
            Session session = (Session) redisTemplate.opsForValue().get(key);
            sessionSet.add(session);
        }
        return sessionSet;
    }
}

将RedisSessionDAO交给SessionManager

@Bean
public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionDAO(sessionDAO);
    return sessionManager;
}

将SessionManager注入到SecurityManager

@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm,SessionManager sessionManager){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(realm);
    securityManager.setSessionManager(sessionManager);
    return securityManager;
}

一次请求,访问了多次redis

解决方案:把请求结果放到request中

你可能感兴趣的:(安全认证,java)