Shiro实战教程笔记

Shiro实战教程笔记

在这里插入图片描述

1. 权限管理

1.1 什么是权限管理

基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或安全策略控制用户可以访问并且只能访问被授权的资源。

1.2 什么是身份认证

身份认证 就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。

1.3 什么是身份授权

身份授权 即访问控制,控制谁能访问那些资源,不同的用户应该拥有不同的资源访问权限,常见的有学校的教务管理系统:有教师,学生,管理员登录几个模块,以不同的身份登录就会显示不同的界面!

2. 什么是shiro

shiro是Apache旗下的一个开源框架,它将软件系统的安全认证相关的功能抽取出来,是一个功能强大且简单易用的Java安全框架,它可以用来进行身份验证,授权,加密和会话管理。

3. shiro核心架构

在这里插入图片描述

3.1 Subject

Subject即主体,外部应用与Subject进行交互,Subject对象记录了当前操作的用户,将用户的概念理解成当前操作的主体,可能是一个浏览器的请求的用户,也可能是一个允许的程序。Subject在shiro中是一个接口,接口中定义了很多认证授权的方法,外部程序通过Subject对象进行认证和授权,而Subject对象是通过SecurityManager进行认证和授权!

3.2 SecurityManager

SecurityManager即安全管理器,对全部的Subject对象进行安全管理,它是shiro的核心,通过SecurityManager可以完成对Subject对象的认证授权等系列操作,实质上是使用Authenticator进行认证,使用Authorizer进行授权,使用SessionManager进行会话管理!

3.3 Authenticator

Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供了ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足认证的大部分需求,也可以使用自定义的认证器。

3.4 Authorizer

Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否由此功能的操作权限。

3.5 Realm

Realm即领域,相当于datasource,SecurityManager进行安全认证是需要从Realm获取用户权限数据,比如:用户的身份信息如果放到数据库中,那么就需要从数据库中获取到用户身份信息。

注:不要把Realm认为只是在里面取数据,在Realm还有认证和授权相关的代码!

3.6 SessionManager

SessionManager即会话管理,shiro框架定义了一系列会话管理,它不依赖于web的session,所以shiro可以使用在非web的环境下,也可以将分布式的应用集中在一起管理,此特性可以实现单点登录!

3.7 SessionDao

SessionDao即会话dao,是对session会话操作的一套接口,比如将session存储到数据库,也可以使用jdbc将session存入数据库。

3.8 CacheManager

CacheManager即缓存管理器,将用户权限数据存储到缓存中,可以提高性能。

3.9 Cryptography

Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发,比如提供了常用分散列,盐值计算,加/解密等功能!

4. shiro的认证

4.1 认证

认证就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。

4.2 认证的关键对象

  • Subject:主体

访问系统的用户,主体可以是程序,用户等,进行认证的都称为主体。

  • Principal:身份信息

是主体进行身份认证的标识,标识必须具有唯一性,如电话号码,手机号,邮箱地址,一个主体可以有多个身份,但必须有一个主身份!

  • credential:凭证信息

只有主体自己知道的安全信息,如密码,证书等。

4.3 认证流程

在这里插入图片描述

4.4 认证的开发

创建项目并引入依赖

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.5.3</version>
    </dependency>
</dependencies>

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <version>3.1.0</version>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

在resources配置shiro.ini核心配置文件

[users]
admin=123456
lizhipeng=111111
huangyuane=222222

测试认证

/**
 * 测试认证
 * @author 隐约雷鸣
 * @date 2021/1/21 11:11
 */
public class TestAuthenticator {
    public static void main(String[] args) {
        // 1.创建安全管理器对象
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2.给安全管理器设置realm(本地IniRealm文件)
        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        // 3.安全工具类设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 4.获取主体对象
        Subject subject = SecurityUtils.getSubject();
        // 5.创建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
        // 6.用户认证
        try {
            subject.login(token);
            System.out.println("认证状态:" + subject.isAuthenticated());
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("认证失败:用户名不存在!");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("认证失败:密码错误!");
        }
    }
}

4.5 自定义Realm的开发

使用本地shiro.ini开发,使用的realm就是我们本地提供的,而实际开发中,realm的数据往往是我们的数据库中读取!

1.shiro提供的Realm

在这里插入图片描述

2.Realm的实现类中认证是使用SimpleAccountRealm

在这里插入图片描述

SimpleAccountRealm 部分源码中有两个方法,分别是

认证:doGetAuthenticationInfo(AuthenticationToken token)

授权:doGetAuthorizationInfo(PrincipalCollection principals)

// 认证
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken)token;
        SimpleAccount account = this.getUser(upToken.getUsername());
        if (account != null) {
            if (account.isLocked()) {
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }

            if (account.isCredentialsExpired()) {
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }
        }

        return account;
    }

// 授权
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = this.getUsername(principals);
        this.USERS_LOCK.readLock().lock();

        AuthorizationInfo var3;
        try {
            var3 = (AuthorizationInfo)this.users.get(username);
        } finally {
            this.USERS_LOCK.readLock().unlock();
        }

        return var3;
    }

3.自定义Realm

public class CustomerRealm extends AuthorizingRealm {
  	// 认证
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
		
  	// 授权
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 在token中获取身份信息
        String principal = (String) token.getPrincipal();
        // 判断获取的身份信息是否与数据库一致,这里暂时使用假数据代替
        if ("admin".equals(principal)) {
            /**
             * 如果用户名通过,返回数据库中用户信息
             * principal:用户名
             * credentials:密码
             * realmName:当前Realm名字
             */
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, "123456", this.getName());
            return authenticationInfo;
        }
        return null;
    }
}

4.使用自定义Realm进行认证

public class TestCustomerRealmAuthenticator {
    public static void main(String[] args) {
        // 创建安全管理器对象
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 给安全管理设置Realm
        securityManager.setRealm(new CustomerRealm());
        // 安全工具类设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 获取主体
        Subject subject = SecurityUtils.getSubject();
        // 设置token
        UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");

        try {
            subject.login(token);
            System.out.println(subject.isAuthenticated());
        } catch (AuthenticationException e) {
            e.printStackTrace();
        }
    }
}

4.6 对密码进行加盐和Hash

1.测试加密,加盐,加散列

public class TestShiroMD5 {
    public static void main(String[] args) {
        // 不加盐和散列次数
        Md5Hash md5Hash1 = new Md5Hash("123456");
        System.out.println(md5Hash1.toHex());
        // 加盐,不加散列次数
        Md5Hash md5Hash2 = new Md5Hash("123456", "X*02");
        System.out.println(md5Hash2.toHex());
        // 加盐,加散列次数
        Md5Hash md5Hash3 = new Md5Hash("123456", "X*02", 1024);
        System.out.println(md5Hash3.toHex());
    }
}

自定义Realm,加入MD5+盐+hash

public class CustomerMd5Realm extends AuthorizingRealm {

    // 认证
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    // 授权
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取身份信息
        String principal = (String) token.getPrincipal();
        if ("admin".equals(principal)) {
            // md5
            // return new SimpleAuthenticationInfo(principal, "e10adc3949ba59abbe56e057f20f883e", this.getName());
            // md5 + 盐
            // return new SimpleAuthenticationInfo(principal, "6c772d52ae59f2cedacf82d0eb60a9ff", ByteSource.Util.bytes("X*02"), this.getName());
            // md5 + 盐 + hash
            return new SimpleAuthenticationInfo(principal, "791113c0ca9bf9cea458f1221d0fb728", ByteSource.Util.bytes("X*02"), this.getName());
        }
        return null;
    }
}

使用自定义Realm进行认证

public class TestCustomerMd5RealmAuthenticator {
    public static void main(String[] args) {
        // 创建安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();

        // 设置Realm使用hash凭证匹配器
        CustomerMd5Realm realm = new CustomerMd5Realm();
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("md5");
        // 适配器设置散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        // 注入Realm
        securityManager.setRealm(realm);

        // 安全管理工具设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 获取主体
        Subject subject = SecurityUtils.getSubject();
        // 设置token
        UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");

        try {
            subject.login(token);
            System.out.println("登录成功:" + subject.isAuthenticated());
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }
    }
}

5. shiro的授权

5.1 授权

授权,即访问控制,控制谁能访问那些资源。

5.2 授权的关键对象

  • Subject:主体

    主体需要访问系统的资源。

  • Resource:资源

    如系统菜单、页面、按钮、类方法、系统商品信息等。

  • Permission: 权限

    规定了主体对资源的操作许可,权限离开了资源没有意义。

5.3 授权的流程

在这里插入图片描述

5.3 授权方式

  • 基于角色的访问控制

    以角色为中心进行访问控制

  • 基于资源的访问控制

    以资源为中心的访问控制

5.5 权限字符串

权限字符串的规则是:资源标识符:操作:资源实例标识符。意思是对哪个资源的那个实例具有什么权限,”:“是分隔符,权限字符串也可以使用“*”通配符。

例子:

用户创建权限: user:create 或 user:create:*
用户修改实例001的权限: user:update:001
用户实例001的所有权限: user:*:001

5.6 shiro中授权的编程实现方式

  • 编程式
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {
  // 有权限
} else {
  // 无权限
}
  • 注解式
@RequiresRoles("admin")
public void hello() {
  // 有权限
}
  • 标签式
<shiro:hasRole name="admin">
  <!-- 有权限 -->
</shiro:hasRole>
注意:Thymeleaf中使用shiro需要额外集成

5.7 开发授权

自定义Realm, 给用户设置权限

public class CustomerMd5Realm extends AuthorizingRealm {

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取主身份信息
        String primaryPrincipal = (String) principals.getPrimaryPrincipal();
        System.out.println("身份信息:" + primaryPrincipal);

        // 根据身份信息获取当前用户的角色信息以及权限信息
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 将数据库查到的用户信息赋值给权限对象
        authorizationInfo.addRole("user");
        authorizationInfo.addRole("super");

        // 将数据库查到的用户信息赋值给权限对象
        authorizationInfo.addStringPermission("user:*:01");
        authorizationInfo.addStringPermission("product:update");
        return authorizationInfo;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取身份信息
        String principal = (String) token.getPrincipal();
        if ("admin".equals(principal)) {
            // md5
            // return new SimpleAuthenticationInfo(principal, "e10adc3949ba59abbe56e057f20f883e", this.getName());
            // md5 + 盐
            // return new SimpleAuthenticationInfo(principal, "6c772d52ae59f2cedacf82d0eb60a9ff", ByteSource.Util.bytes("X*02"), this.getName());
            // md5 + 盐 + hash
            return new SimpleAuthenticationInfo(principal, "791113c0ca9bf9cea458f1221d0fb728", ByteSource.Util.bytes("X*02"), this.getName());
        }
        return null;
    }
}

使用自定义Realm进行认证授权

public class TestCustomerMd5RealmAuthenticator {
    public static void main(String[] args) {
        // 创建安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();

        // 设置Realm使用hash凭证匹配器
        CustomerMd5Realm realm = new CustomerMd5Realm();
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("md5");
        // 适配器设置散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        // 注入Realm
        securityManager.setRealm(realm);

        // 安全管理工具设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 获取主体
        Subject subject = SecurityUtils.getSubject();
        // 设置token
        UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");

        try {
            subject.login(token);
            System.out.println("登录成功:" + subject.isAuthenticated());
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }

        // 授权
        if (subject.isAuthenticated()) {
            // 基于单个角色的权限控制
            System.out.println("权限:" + subject.hasRole("user"));
            // 基于多个角色的权限控制
            System.out.println("权限:" + subject.hasAllRoles(Arrays.asList("user", "super")));
            // 是否具有其中一个角色
            boolean[] booleans = subject.hasRoles(Arrays.asList("user", "admin", "super"));
            for (boolean aBoolean : booleans) {
                System.out.println(aBoolean);
            }

            System.out.println("================");

            // 基于权限字符串的访问控制
            System.out.println("权限:" + subject.isPermitted("user:create:01"));
            System.out.println("权限:" + subject.isPermitted("product:update:02"));
            // 分别具有哪些权限
            boolean[] permitted = subject.isPermitted("user:create:01", "order:create:01");
            for (boolean b : permitted) {
                System.out.println(b);
            }
            // 同时具有哪些权限
            boolean permittedAll = subject.isPermittedAll("user:*:01", "product:update");
            System.out.println(permittedAll);
        }
    }
}

6. SpringBoot整合shiro

整合思路
在这里插入图片描述

6.1 项目准备

导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--jsp解析依赖-->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

配置application.yml

server:
  port: 8888
  servlet:
    context-path: /shiro
spring:
  application:
    name: shiro
  mvc:
    view:
      prefix: /
      suffix: .jsp

在webapp下配置jsp页面

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>index.jsp</title>
</head>
<body>
    <h1>系统管理</h1>
    <ul>
        <li>用户管理</li>
        <li>商品管理</li>
        <li>订单管理</li>
        <li>物流管理</li>
    </ul>
</body>
</html>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>login.jsp</title>
</head>
<body>
    <h1>登录页面</h1>
</body>
</html>

启动类

@SpringBootApplication
public class SpringbootJspShiroApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootJspShiroApplication.class, args);
    }

}

6.2 整合shiro

导入依赖

<!--springboot整合shiro依赖-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.5.3</version>
</dependency>

创建shiro配置类

@Configuration
public class ShiroConfig {
    /**
     * 1.创建ShiroFilter
     *
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
      	// 配置系统受限资源
        // 配置系统公共资源
        Map<String, String> map = new HashMap<>();
        // authc:请求这个资源需要认证和授权 anon:请求这个资源不需要认证和授权
        map.put("/user/login", "anon");
        map.put("/user/register", "anon");
        map.put("/**", "authc");
        // 默认认证界面
        shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * 2. 创建SecurityManager
     *
     * @param realm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("realm") Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        // 设置Realm
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    /**
     * 3.创建自定义Realm
     *
     * @return
     */
    @Bean(name = "realm")
    public Realm getRealm() {
        CustomerRealm customerRealm = new CustomerRealm();
        return customerRealm;
    }

}

自定义Realm

/**
 * 自定义Realm
 *
 * @author 隐约雷鸣
 * @date 2021/1/27 09:39
 */
public class CustomerRealm extends AuthorizingRealm {
    /**
     * 授权
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    /**
     * 认证
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        return null;
    }
}

测试运行是否成功,如果没报错则证明整合shiro成功,访问index.jsp页面时会自动跳转login.jsp页面

6.3 shiro常见过滤器

配置缩写对应的过滤器功能
身份验证相关的
anonAnonymousFilter指定url可以匿名访问
authcFormAuthenticationFilter基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储key(shiroLoginFailure)
authcBasicBasicHttpAuthenticationFilterBasic HTTP身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application)
logoutauthc.LogoutFilter退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/)
userUserFilter用户拦截器,用户已经身份验证/记住我登录的都可
授权相关的
rolesRolesAuthorizationFilter角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]”
permsPermissionsAuthorizationFilter权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms[“user:create”]”
portPortFilter端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
restHttpMethodPermissionFilterrest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll)
sslSslFilterSSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样
noSessionCreationNoSessionCreationAuthorizationFilter需要指定权限才能访问

6.4 登录功能实现

login.jsp

<body>
    <h1>登录页面</h1>
    <form method="post" action="${pageContext.request.contextPath}/user/login" >
        用户名:<input name="username" type="text"> <br/>
        密码:  <input name="password" type="password"> <br/>
        <input type="submit" value="登录">
    </form>
</body>

controller

@Controller
@RequestMapping("user")
public class UserController {

    /**
     * 登录
     *
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/login")
    public String login(String username, String password) {
        // 获取主体
        Subject subject = SecurityUtils.getSubject();
        // 登录
        try {
            subject.login(new UsernamePasswordToken(username, password));
            return "redirect:/index.jsp";
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误!");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误!");
        }
        return "redirect:/login.jsp";
    }
}

自定义Realm简单实现认证

/**
 * 认证
 *
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    String principal = (String) token.getPrincipal();
    if ("admin".equals(principal)) {
        return new SimpleAuthenticationInfo(principal, "123456", this.getName());
    }
    return null;
}

6.5 注销功能实现

Index.jsp

<a href="${pageContext.request.contextPath}/user/logout">注销</a>

controller

/**
 * 注销
 *
 * @return
 */
@GetMapping("/logout")
public String logout() {
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "redirect:/login.jsp";
}

6.6 基于数据库实现用户注册功能

创建用户表

CREATE TABLE `t_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(40) DEFAULT NULL,
  `password` varchar(40) DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

添加依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.2</version>
</dependency>

yml配置

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///shiro?useUniCode=true&useSSL=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 1234567890
mybatis:
  type-aliases-package: com.lzp.springboot_jsp_shiro.entity
  mapper-locations: classpath:com/lzp/mapper/*.xml

准备注册页面

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>register.jsp</title>
</head>
<body>
    <h1>注册页面</h1>
    <form method="post" action="${pageContext.request.contextPath}/user/register">
        用户名:<input name="username" type="text"> <br/>
        密码:  <input name="password" type="password"> <br/>
        <input type="submit" value="立即注册">
    </form>
</body>
</html>

用户实体类

@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String username;
    private String password;
    private String salt;
}

生成随机盐工具类

public class SaltUtils {
    
    /**
     * 生成随机盐
     *
     * @param n
     * @return
     */
    public static String getSalt(int n) {
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()".toCharArray();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char aChar = chars[new Random().nextInt(chars.length)];
            sb.append(aChar);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println(getSalt(4));
    }
}

controller

/**
 * 注册
 *
 * @param user
 * @return
 */
@PostMapping("/register")
public String register(User user) {
    try {
        userService.save(user);
        return "redirect:/login.jsp";
    } catch (Exception e) {
        e.printStackTrace();
        return "redirect:/register.jsp";
    }
}

service

public interface UserService {
    /**
     * 保存用户
     *
     * @param user
     */
    void save(User user);
}
@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 保存用户
     * @param user
     */
    @Override
    public void save(User user) {
        // 生成随机盐
        String salt = SaltUtils.getSalt(8);
        user.setSalt(salt);
        // 明文密码进行md5 + 盐 + hash
        Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
        user.setPassword(md5Hash.toHex());
        userMapper.save(user);
    }
}

mapper

@Mapper
@Repository
public interface UserMapper {
    /**
     * 保存用户
     */
    void save(User user);
}
<mapper namespace="com.lzp.springboot_jsp_shiro.mapper.UserMapper">
    <insert id="save" parameterType="User" useGeneratedKeys="true" keyProperty="id">
        insert into t_user values (#{id}, #{username}, #{password}, #{salt});
    </insert>
</mapper>

6.7 基于数据库重新实现用户认证功能

自定义Realm修改认证逻辑

/**
 * 认证
 *
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    String principal = (String) token.getPrincipal();
    User user = userService.findByUserName(principal);
    if (user != null) {
        return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
    }
    return null;
}

service

/**
 * 根据用户名查询
 *
 * @param principal
 */
User findByUserName(String principal);
/**
 * 根据用户名查询
 *
 * @param username
 */
@Override
public User findByUserName(String username) {
    return userMapper.findByUserName(username);
}

mapper

/**
 * 根据用户名查询
 *
 * @param username
 */
User findByUserName(String username);
<select id="findByUserName" parameterType="String" resultType="User">
    select * from t_user where username = #{username};
</select>

ShiroConfig修改凭证匹配器,适用于MD5 + 盐 + Hash

@Bean(name = "realm")
public Realm getRealm() {
    CustomerRealm customerRealm = new CustomerRealm();
    // 修改凭证校验匹配器
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    // 设置加密算法为MD5
    credentialsMatcher.setHashAlgorithmName("MD5");
    // 设置hash散列次数
    credentialsMatcher.setHashIterations(1024);
  	customerRealm.setCredentialsMatcher(credentialsMatcher);
    return customerRealm;
}

6.8 授权开发

1.前端页面的授权(基于角色的访问控制)

自定义Realm设置用户对应角色权限

/**
 * 授权
 *
 * @param principals
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 获取身份信息
    String primaryPrincipal = (String) principals.getPrimaryPrincipal();
    // 获取角色信息
    if ("admin".equals(primaryPrincipal)) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 给admin用户添加admin角色权限
        authorizationInfo.addRole("admin");
        return authorizationInfo;
    }
    return null;
}

jsp页面添加标签控制访问权限

<body>
    <h1>系统管理</h1>
    <a href="${pageContext.request.contextPath}/user/logout">注销</a>
    <ul>
      	<!-- 用户管理只允许user,admin用户看到 -->
        <shiro:hasAnyRoles name="user,admin">
            <li><a href="">用户管理</li>
        </shiro:hasAnyRoles>
        <shiro:hasRole name="admin">
            <li><a href="">商品管理</li>
            <li><a href="">订单管理</li>
            <li><a href="">物流管理</li>
      	</shiro:hasRole>
    </ul>
</body>
2.前端页面的授权(基于权限字符串的访问控制)

自定义Realm设置用户角色的权限字符串

/**
 * 授权
 *
 * @param principals
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 获取身份信息
    String primaryPrincipal = (String) principals.getPrimaryPrincipal();
    // 获取角色信息
    if ("admin".equals(primaryPrincipal)) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 基于角色的权限管理:给admin用户添加admin角色权限
        authorizationInfo.addRole("user");
        // 基于权限字符串的权限控制:给用户角色添加用户管理的所有权限
        authorizationInfo.addStringPermission("user:*:*");
        return authorizationInfo;
    }
    return null;
}

jsp页面添加标签控制访问权限

<body>
    <h1>系统管理</h1>
    <a href="${pageContext.request.contextPath}/user/logout">注销</a>
    <ul>
        <shiro:hasAnyRoles name="user,admin">
            <li><a href="">用户管理</a>
                <ul>
                    <shiro:hasPermission name="user:create:*">
                        <li><a href="">添加用户</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:update:*">
                        <li><a href="">修改用户</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:delete:*">
                        <li><a href="">删除用户</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:select:*">
                        <li><a href="">查询用户</a></li>
                    </shiro:hasPermission>
                </ul>
            </li>
        </shiro:hasAnyRoles>
        <shiro:hasRole name="admin">
            <li><a href="">商品管理</a></li>
            <li><a href="">订单管理</a></li>
            <li><a href="">物流管理</a></li>
        </shiro:hasRole>
    </ul>
</body>
3.后端的授权(基于角色的访问控制)

controller

@Controller
@RequestMapping("order")
public class OrderController {

    @GetMapping("/create")
    public String createOrder() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.hasRole("admin")) {
            System.out.println("保存订单");
        } else {
            System.out.println("无权访问");
        }
        return "redirect:/index.jsp";
    }
}

自定义Realm设置用户角色为admin

/**
 * 授权
 *
 * @param principals
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 获取身份信息
    String primaryPrincipal = (String) principals.getPrimaryPrincipal();
    // 获取角色信息
    if ("admin".equals(primaryPrincipal)) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 基于角色的权限管理:给admin用户添加admin角色权限
        authorizationInfo.addRole("admin");
        // 基于权限字符串的权限控制:给用户角色添加用户管理的所有权限
        authorizationInfo.addStringPermission("user:*:*");
        return authorizationInfo;
    }
    return null;
}

注解方式

@GetMapping("/update")
// @RequiresRoles("admin") // 要求admin角色才能访问
@RequiresRoles(value = {"admin", "user"}) // 同时有admin和user角色才能访问
public String updateOrder() {
    System.out.println("更新成功");
    return "redirect:/index.jsp";
}
4.后端的授权(基于权限字符串的访问控制)

controller

@GetMapping("/delete")
@RequiresPermissions("user:delete:*") // 要求该用户有user:delete:*权限
public String deleteOrder() {
    System.out.println("删除成功");
    return "redirect:/index.jsp";
}

6.9 授权数据持久化

在这里插入图片描述

创建数据库表

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80020
 Source Host           : localhost:3306
 Source Schema         : shiro

 Target Server Type    : MySQL
 Target Server Version : 80020
 File Encoding         : 65001

 Date: 31/01/2021 22:09:41
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_perms`;
CREATE TABLE `t_perms` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(80) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(60) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
  `id` int NOT NULL AUTO_INCREMENT,
  `roleid` int DEFAULT NULL,
  `permsid` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(40) DEFAULT NULL,
  `password` varchar(40) DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `userid` int DEFAULT NULL,
  `roleid` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

SET FOREIGN_KEY_CHECKS = 1;

创建对应实体类并建立对应关系

@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String username;
    private String password;
    private String salt;
    // 定义角色集合
    private List<Role> roles;
}
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private Integer id;
    private String name;
  	// 定义权限集合
    private List<Perms> permsList;
}
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class Perms {
    private Integer id;
    private String name;
    private String url;
}

6.10 基于数据库重新实现授权流程

自定义Realm修改授权流程

/**
 * 授权
 *
 * @param principals
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 获取身份信息
    String primaryPrincipal = (String) principals.getPrimaryPrincipal();
    // 获取角色信息
    User user = userService.findRolesByUserName(primaryPrincipal);
    if (!CollectionUtils.isEmpty(user.getRoles())) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        user.getRoles().forEach(role -> {
            authorizationInfo.addRole(role.getName());
            // 根据角色id设置对应的权限信息
            List<Perms> permsList = userService.findPermsListByRoleId(role.getId());
            if (!CollectionUtils.isEmpty(permsList)) {
                permsList.forEach(perms -> {
                    authorizationInfo.addStringPermission(perms.getName());
                });
            }
        });
        return authorizationInfo;
    }
    return null;
}

service

/**
 * 根据用户名获取所有角色信息
 *
 * @param primaryPrincipal
 * @return
 */
User findRolesByUserName(String primaryPrincipal);

/**
 * 根据角色id查询所有权限信息
 *
 * @param id
 * @return
 */
List<Perms> findPermsListByRoleId(Integer id);
/**
 * 根据用户名获取所有角色信息
 *
 * @param username
 * @return
 */
@Override
public User findRolesByUserName(String username) {
    return userMapper.findRolesByUserName(username);
}

/**
 * 根据角色id查询所有权限信息
 *
 * @param id
 * @return
 */
@Override
public List<Perms> findPermsListByRoleId(Integer id) {
    return userMapper.findPermsListByRoleId(id);
}

mapper

/**
 * 根据用户名获取所有角色信息
 *
 * @param username
 * @return
 */
User findRolesByUserName(String username);

/**
 * 根据角色id查询所有权限信息
 *
 * @param id
 * @return
 */
List<Perms> findPermsListByRoleId(Integer id);
<resultMap id="userMap" type="User">
    <id column="uid" property="id"></id>
    <result column="username" property="username"></result>
    <!-- 角色信息 -->
    <collection property="roles" javaType="List" ofType="Role">
        <id column="rid" property="id"></id>
        <result column="rname" property="name"></result>
    </collection>
</resultMap>

<select id="findRolesByUserName" parameterType="String" resultMap="userMap">
    SELECT u.id uid, u.username, r.id rid, r.`name` rname FROM t_user u
    LEFT JOIN t_user_role ur
    ON u.id =  ur.userid
    LEFT JOIN t_role r
    ON ur.roleid = r.id
    WHERE u.username = #{username}
</select>

<select id="findPermsListByRoleId" parameterType="Integer" resultType="Perms">
    SELECT r.`name` rname, p.id, p.name, p.url FROM t_role r
    LEFT JOIN t_role_perms rp
    ON r.id = rp.roleid
    LEFT JOIN t_perms p
    ON rp.permsid = p.id
    WHERE r.id = #{id}
</select>

7. 缓存管理器

在这里插入图片描述

7.1 使用默认EhCache实现缓存

引入依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.5.3</version>
</dependency>

ShiroConfig中配置缓存管理器

/**
 * 3.创建自定义Realm
 *
 * @return
 */
@Bean(name = "realm")
public Realm getRealm() {
    CustomerRealm customerRealm = new CustomerRealm();
    // 修改凭证校验匹配器
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    // 设置加密算法为MD5
    credentialsMatcher.setHashAlgorithmName("MD5");
    // 设置hash散列次数
    credentialsMatcher.setHashIterations(1024);
    customerRealm.setCredentialsMatcher(credentialsMatcher);
    // 开启缓存管理器
    customerRealm.setCacheManager(new EhCacheManager());
    customerRealm.setCachingEnabled(true);
    customerRealm.setAuthenticationCachingEnabled(true);
    customerRealm.setAuthenticationCacheName("authenticationCache");
    customerRealm.setAuthorizationCachingEnabled(true);
    customerRealm.setAuthorizationCacheName("authorizationCache");
    return customerRealm;
}

7.2 使用Redis实现缓存

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

yml新增redis配置

spring:
  redis:
    port: 6379
    host: localhost
    database: 0

自定义RedisCacheManager实现CacheManage

public class RedisCacheManager implements CacheManager {

    /**
     * @param cacheName 认证或者是授权缓存的统一名称
     * @param <K>
     * @param <V>
     * @return
     * @throws CacheException
     */
    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        System.out.println("cacheName:" + cacheName);
        return new RedisCache<K, V>(cacheName);
    }
}

自定义RedisCache实现Cache

public class RedisCache<k, v> implements Cache<k, v> {

    private String cacheName;

    public RedisCache() {
    }

    public RedisCache(String cacheName) {
        this.cacheName = cacheName;
    }

    @Override
    public v get(k k) throws CacheException {
        System.out.println("get:key: " + k);
        return (v) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
    }

    @Override
    public v put(k k, v v) throws CacheException {
        System.out.println("put:key: " + k);
        System.out.println("put:value: " + v);
        getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v);
        return null;
    }

    @Override
    public v remove(k k) throws CacheException {
        return (v) getRedisTemplate().opsForHash().delete(this.cacheName, k.toString());
    }

    @Override
    public void clear() throws CacheException {
        getRedisTemplate().delete(this.cacheName);
    }

    @Override
    public int size() {
        return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
    }

    @Override
    public Set<k> keys() {
        return getRedisTemplate().opsForHash().keys(this.cacheName);
    }

    @Override
    public Collection<v> values() {
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }

    /**
     * 获取RedisTemplate
     *
     * @return
     */
    private RedisTemplate getRedisTemplate() {
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

ApplicationContextUtils工具类,因为无法自动注入redisTemplate

@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    //根据bean名称从工厂中获取bean对象
    public static Object getBean(String name){
        return  applicationContext.getBean(name);
    }
}

自定义Salt的实现,实现序列化接口,提供无参构造,不然会报错反序列化异常,其它实体类也需要实现序列化接口

public class MyByteSource implements ByteSource, Serializable {
    private byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public MyByteSource() {
    }

    public MyByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public MyByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public MyByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public MyByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public MyByteSource(File file) {
        this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
    }

    public MyByteSource(InputStream stream) {
        this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public byte[] getBytes() {
        return this.bytes;
    }

    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }

        return this.cachedHex;
    }

    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    public String toString() {
        return this.toBase64();
    }

    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource)o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }
}

CustomerRealm认证时使用自定义的salt实现

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    String principal = (String) token.getPrincipal();
    User user = userService.findByUserName(principal);
    if (user != null) {
        return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), new MyByteSource(user.getSalt()), this.getName());
    }
    return null;
}

修改ShiroConfig,使用RedisCacheManager实现

@Bean(name = "realm")
public Realm getRealm() {
    CustomerRealm customerRealm = new CustomerRealm();
    // 修改凭证校验匹配器
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    // 设置加密算法为MD5
    credentialsMatcher.setHashAlgorithmName("MD5");
    // 设置hash散列次数
    credentialsMatcher.setHashIterations(1024);
    customerRealm.setCredentialsMatcher(credentialsMatcher);
    // 开启缓存管理器
    customerRealm.setCacheManager(new RedisCacheManager());
    customerRealm.setCachingEnabled(true);
    customerRealm.setAuthenticationCachingEnabled(true);
    customerRealm.setAuthenticationCacheName("authenticationCache");
    customerRealm.setAuthorizationCachingEnabled(true);
    customerRealm.setAuthorizationCacheName("authorizationCache");
    return customerRealm;
}

测试登录

登录成功后,会在Redis中存入authenticationCache和com.lzp.springboot_jsp_shiro.shiro.realms.CustomerRealm.authorizationCache这两个key

image-20210203144008837

7.3 验证码登录

login.jsp

<body>
    <h1>登录页面</h1>
    <form method="post" action="${pageContext.request.contextPath}/user/login" >
        用户名:<input name="username" type="text"> <br/>
        密码:  <input name="password" type="password"> <br/>
        验证码:<input type="text" name="cdoe"><img src="${pageContext.request.contextPath}/user/getImage" alt=""><br>
        <input type="submit" value="登录">
    </form>
</body>

验证码工具类

public class VerifyCodeUtils {

    //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
    public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
    private static Random random = new Random();

    /**
     * 使用系统默认字符源生成验证码
     *
     * @param verifySize 验证码长度
     * @return
     */
    public static String generateVerifyCode(int verifySize) {
        return generateVerifyCode(verifySize, VERIFY_CODES);
    }

    /**
     * 使用指定源生成验证码
     *
     * @param verifySize 验证码长度
     * @param sources    验证码字符源
     * @return
     */
    public static String generateVerifyCode(int verifySize, String sources) {
        if (sources == null || sources.length() == 0) {
            sources = VERIFY_CODES;
        }
        int codesLen = sources.length();
        Random rand = new Random(System.currentTimeMillis());
        StringBuilder verifyCode = new StringBuilder(verifySize);
        for (int i = 0; i < verifySize; i++) {
            verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
        }
        return verifyCode.toString();
    }

    /**
     * 生成随机验证码文件,并返回验证码值
     *
     * @param w
     * @param h
     * @param outputFile
     * @param verifySize
     * @return
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, outputFile, verifyCode);
        return verifyCode;
    }

    /**
     * 输出随机验证码图片流,并返回验证码值
     *
     * @param w
     * @param h
     * @param os
     * @param verifySize
     * @return
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, os, verifyCode);
        return verifyCode;
    }

    /**
     * 生成指定验证码图像文件
     *
     * @param w
     * @param h
     * @param outputFile
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
        if (outputFile == null) {
            return;
        }
        File dir = outputFile.getParentFile();
        if (!dir.exists()) {
            dir.mkdirs();
        }
        try {
            outputFile.createNewFile();
            FileOutputStream fos = new FileOutputStream(outputFile);
            outputImage(w, h, fos, code);
            fos.close();
        } catch (IOException e) {
            throw e;
        }
    }

    /**
     * 输出指定验证码图片流
     *
     * @param w
     * @param h
     * @param os
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
        int verifySize = code.length();
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Random rand = new Random();
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        Color[] colors = new Color[5];
        Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
                Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
                Color.PINK, Color.YELLOW};
        float[] fractions = new float[colors.length];
        for (int i = 0; i < colors.length; i++) {
            colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
            fractions[i] = rand.nextFloat();
        }
        Arrays.sort(fractions);

        g2.setColor(Color.GRAY);// 设置边框色
        g2.fillRect(0, 0, w, h);

        Color c = getRandColor(200, 250);
        g2.setColor(c);// 设置背景色
        g2.fillRect(0, 2, w, h - 4);

        //绘制干扰线
        Random random = new Random();
        g2.setColor(getRandColor(160, 200));// 设置线条的颜色
        for (int i = 0; i < 20; i++) {
            int x = random.nextInt(w - 1);
            int y = random.nextInt(h - 1);
            int xl = random.nextInt(6) + 1;
            int yl = random.nextInt(12) + 1;
            g2.drawLine(x, y, x + xl + 40, y + yl + 20);
        }

        // 添加噪点
        float yawpRate = 0.05f;// 噪声率
        int area = (int) (yawpRate * w * h);
        for (int i = 0; i < area; i++) {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            int rgb = getRandomIntColor();
            image.setRGB(x, y, rgb);
        }

        shear(g2, w, h, c);// 使图片扭曲

        g2.setColor(getRandColor(100, 160));
        int fontSize = h - 4;
        Font font = new Font("Algerian", Font.ITALIC, fontSize);
        g2.setFont(font);
        char[] chars = code.toCharArray();
        for (int i = 0; i < verifySize; i++) {
            AffineTransform affine = new AffineTransform();
            affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
            g2.setTransform(affine);
            g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
        }

        g2.dispose();
        ImageIO.write(image, "jpg", os);
    }

    private static Color getRandColor(int fc, int bc) {
        if (fc > 255)
            fc = 255;
        if (bc > 255)
            bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

    private static int getRandomIntColor() {
        int[] rgb = getRandomRgb();
        int color = 0;
        for (int c : rgb) {
            color = color << 8;
            color = color | c;
        }
        return color;
    }

    private static int[] getRandomRgb() {
        int[] rgb = new int[3];
        for (int i = 0; i < 3; i++) {
            rgb[i] = random.nextInt(255);
        }
        return rgb;
    }

    private static void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }

    private static void shearX(Graphics g, int w1, int h1, Color color) {
        
        int period = random.nextInt(2);

        boolean borderGap = true;
        int frames = 1;
        int phase = random.nextInt(2);
        
        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            if (borderGap) {
                g.setColor(color);
                g.drawLine((int) d, i, 0, i);
                g.drawLine((int) d + w1, i, w1, i);
            }
        }

    }

    private static void shearY(Graphics g, int w1, int h1, Color color) {
        
        int period = random.nextInt(40) + 10; // 50;

        boolean borderGap = true;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            if (borderGap) {
                g.setColor(color);
                g.drawLine(i, (int) d, i, 0);
                g.drawLine(i, (int) d + h1, i, h1);
            }
            
        }
        
    }

    public static void main(String[] args) throws IOException {
        File dir = new File("D:/upload/verifyCode");
        int w = 200, h = 80;
        for (int i = 0; i < 50; i++) {
            String verifyCode = generateVerifyCode(4);
            File file = new File(dir, verifyCode + ".jpg");
            outputImage(w, h, file, verifyCode);
        }
    }
}

controller

/**
 * 登录
 *
 * @param username
 * @param password
 * @return
 */
@PostMapping("/login")
public String login(String username, String password, String code, HttpSession session) {
    try {
        // 校验验证码
        String codes = (String) session.getAttribute("code");
        if (codes.equalsIgnoreCase(code)) {
            // 获取主体
            Subject subject = SecurityUtils.getSubject();
            subject.login(new UsernamePasswordToken(username, password));
            return "redirect:/index.jsp";
        } else {
            throw new RuntimeException("验证码错误!");
        }
    } catch (UnknownAccountException e) {
        e.printStackTrace();
        System.out.println("用户名错误!");
    } catch (IncorrectCredentialsException e) {
        e.printStackTrace();
        System.out.println("密码错误!");
    } catch (Exception e) {
        e.printStackTrace();
        System.out.println(e.getMessage());
    }
    return "redirect:/login.jsp";
}

/**
 * 验证码
 *
 * @param session
 * @param response
 */
@GetMapping("/getImage")
public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
    // 生成验证码存入session
    String code = VerifyCodeUtils.generateVerifyCode(4);
    session.setAttribute("code", code);
    ServletOutputStream outputStream = response.getOutputStream();
    // 验证码存入图片
    response.setContentType("image/png");
    VerifyCodeUtils.outputImage(200, 30, outputStream, code);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值