Spring Boot整合Shiro + JSP教程(用户认证,权限管理,图片验证码)

在此首先感谢**编程不良人**up主提供的视频教程
代码都是跟着up的视频敲的,遇到的一些问题也是通过CSDN博主提供的教程解决的,在此也感谢那些提供bug解决方案的前辈们~
项目完整代码已经发布到github上面,有需要的朋友可以自取


1.权限管理

1.1 什么是权限管理

​ 涉及到用户参与的系统都要涉及权限管理,权限管理属于系统安全范畴。权限管理实现的是对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

​ 权限管理包括 用户身份认证授权 两部分,首先对用户进行身份认证,认证通过后按照授权规则允许用户访问对应资源。

1.2 什么是身份认证

​ 身份认证,即判断用户是否是合法用户。常见的认证方式有:登录账号+密码,指纹识别, 人脸识别,射频卡…

1.3 什么是授权

​ 授权,即访问控制,允许不同权限级别的用户可以访问系统中的对应的系统资源

2.Shiro核心架构

Shiro是一个功能强大且易用的Java安全框架,它集成了用户身份认证,权限授权,加密,会话管理等功能。

2.1 Subject

​ Subject是一个概念主体,它可以是一个正在浏览网页程序的用户,也可能是一个运行中的程序。

​ Subject是Shiro中的一个接口,外部程序通过subject获得认证和授权,而subject则通过SecurityManager安全管理器进行认证授权。

2.2 Security Manager

​ Security Manager 是安全管理器,对全部的subject进行安全管理。**它是Shiro的核心通过Security Manager 实现subject的认证和授权。实际上,Security Manager 是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager**进行会话管理。

SecurityManager继承了Authenticator,Authorizer,SessionManager三个接口。

2.3 Authenticator

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

2.4 Authorizer

​ 用户在获得认证器的认证以后,需要通过授权器界定用户可以访问那些功能

2.5 Realm

Realm即领域,相当于datasource数据源,SecurityManager对用户进行安全认证需要通过Realm获取用户数据。SecurityManager通过Realm实现对数据库数据的获取。但Realm不仅仅是获取数据,在Realm中也有认证等功能。

2.6 SessionManager

​ SessionManager即会话管理器,该会话管理器不依赖于web容器的session,因此shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一处管理,通过该特性实现单点登录。

2.7 SessionDao

​ SessionDao即会话Dao,是对session会话操作的一套接口,可以通过该接口实现会话的数据库存储。

2.8 CacheManager

​ 缓存管理,将用户的权限数据存储在缓存,这样减少数据库IO,提高系统性能。

2.9 CryptoGraphy

​ 密码管理,是一套加密解密组件,方便开发。提供常见的散列,加/解密功能。

3.Shiro中的认证

3.1认证的关键对象与流程

身份认证就是判断一个用户是否为合法用户的过程。常用的认证方式有账户和口令,通过匹配系统中的账户和口令来判断用户是否合法。

  • **subject:**主体,访问系统的主体(用户,程序)

  • **Principal:**身份信息,是主体身份标识,标识必须具有唯一性。例如:手机号码,邮箱,用户名等;一个用户可以有多个身份信息,但是必须要有一个主身份信息-Primary Principal

  • Crendential:凭证信息,只有主体知道的安全信息,比如密码,证书等等。

3.2 认证开发

3.2.1简单认证demo

创建maven项目,导入shiro坐标:

        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-coreartifactId>
            <version>1.5.3version>
        dependency>

设置shiro.ini,配置用户信息

[users]
zhangsan=123
wangwu=456
lisi=789

创建TestAuthenticator类

package im.hwp;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;

public class TestAuthenticator {
    public static void main(String[] args) {
        //1.首先获取SecurityManager对象
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //2.设置defaultSecurityManager对象的Realm
        defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        //3.给SecurityUtils全局工具类绑定defaultSecurityManager对象
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //4.通过SecurityUtils类获取Subject主体
        Subject subject = SecurityUtils.getSubject();
        //5. 设置登录信息,创建登录令牌
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","1231");
        //6. 使用该令牌进行登录
        try{
            System.out.println(subject.isAuthenticated());
            subject.login(token);
            System.out.println(subject.isAuthenticated());
        }catch(UnknownAccountException exception){
            //在认证过程中,如果用户名不存在,会抛出UnknownAccountException
            exception.printStackTrace();
            System.out.println("认证失败:用户名不存在");
        }catch(IncorrectCredentialsException exception){
            //认证过程中,如果用户名正常而密码错误,会抛出IncorrectCredentialsException
            exception.printStackTrace();
            System.out.println("认证失败:密码错误");
        }
    }
}

3.2.2 认证流程的源码分析

阅读源码后发现,真正实现用户名(身份信息)检查的类是SimpleAccountRealm.class,它继承自AuthorizingRealm

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //将token抢转为UsernamePasswordToken
        UsernamePasswordToken upToken = (UsernamePasswordToken)token;
        //根据用户名获取到account的信息,其中包括登录密码
        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;
    }

在检查用户名合法以后,会继续核验用户的密码是否正确,该方法assertCredentialsMatchAuthenticatingRealm类中实现。

protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        CredentialsMatcher cm = this.getCredentialsMatcher();
        if (cm != null) {
            if (!cm.doCredentialsMatch(token, info)) {
                String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
                throw new IncorrectCredentialsException(msg);
            }
        } else {
            throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication.  If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
        }
    }

分别获取用户提供的密码和系统查询到的密码,并实现比较的代码:

public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenCredentials = this.getCredentials(token);
        Object accountCredentials = this.getCredentials(info);
        return this.equals(tokenCredentials, accountCredentials);
    }

AuthenticatingRealm ——> doGetAuthenticationInfo, 该方法用于认证管理

AuthorizingRealm ——> doGetAuthorizationInfo,该方法用于授权管理

在后期自己开发Realm时,需要定义一个类,该类继承AuthorizingRealm,并重写其中doGetAuthenticationInfo和doGetAuthorizationInfo方法。

3.2.3 使用MD5 + Salt 算法实现认证功能

realm中方法通过传入的token获取到principal(用户名),使用用户名去数据库获取用户的密码。将用户的用户名和密码封装到SimpleAuthenticationInfo对象,由shiro执行密码检查。

要点解析:

a.要实现MD5 + Salt,需要在用户初次注册时,对密码进行MD5 + Salt运算,此时存入数据库的数据是已经加密过的。

b.在实现Realm时,需要在Realm中认证方法返回值SimpleAuthenticationInfo定义用户身份信息,密码,随机盐以及realm对象

    //认证
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String principal = (String) authenticationToken.getPrincipal();
        return new SimpleAuthenticationInfo(
                principal,
                "752ed83bdebff1a1566c20ca2ff4b164",
                ByteSource.Util.bytes("hwp"),
                this.getName()
        );
    }

c.为realm对象绑定密码验证器,需要指定密码验证器的加密算法和hash次数

public static void main(String[] args) {
//      //1.创建realm对象
        CustomerRealm customerRealm = new CustomerRealm();
        //2. 设置realm的比较器
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        hashedCredentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        //3. 创建SecurityManager对象
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //4.设置defaultSecurityManager对象的Realm
        defaultSecurityManager.setRealm(customerRealm);
        //5.给SecurityUtils全局工具类绑定defaultSecurityManager对象
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //4.通过SecurityUtils类获取Subject主体
        Subject subject = SecurityUtils.getSubject();
        //5. 设置登录信息,创建登录令牌
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123");
        //6. 使用该令牌进行登录
        try{
            System.out.println(subject.isAuthenticated());
            subject.login(token);
            System.out.println(subject.isAuthenticated());
        }catch(UnknownAccountException exception){
            exception.printStackTrace();
            System.out.println("认证失败:用户名不存在");
        }catch(IncorrectCredentialsException exception){
            exception.printStackTrace();
            System.out.println("认证失败:密码错误");
        }
}

4.Shiro的授权

4.1 概念

​ 授权,即认证之后的用户进行权限管理。主要是对用户进行系统资源访问权限的授权。

4.2 关键对象

授权可以简单理解为who对what进行How操作

​ Who:主体(Subject)

​ What:系统资源(Resource),如系统方法,页面,按钮,系统商品信息等。资源包括资源类型资源实例

​ How:权限/许可(Permission),规定了主体对资源的操作许可。

4.3 授权方式

  • 基于角色

    基于角色的访问控制,是以角色为中心进行访问控制

    if(subject.hasRole("admin")){
        //操作资源
    }
    
  • 基于资源

    基于资源的访问控制,是以资源为中心进行访问控制

    if(subject.isPermission("user:create:*")){
        //所有的用户具有创建权限
    }
    

4.4 权限字符串

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

例子:

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

4.6 Shiro中授权编程的方式

  • 编程式

    Subject subject = SecurityUtils.getSubject();
    if(subject.hasRole("admin")){
        //有权限
    }else{
        //没有权限
    }
    
  • 注解式

    @RequiresRoles("admin")
    public void hello(){
        //有权限
    }
    
  • 标签式

    
    	
    
    

4.7 Shiro开发认证

权限的获取部分:

    //在自定义的Realm类中设置授权
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取当前的用户名
        Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
        //获取用户名以后,应该去缓存或者数据库查询出该用户对应的权限信息
        // 创建simpleAuthorizationInfo对象,并将查询到的角色信息添加到该对象中,然后返回该对象。
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");
         //基于权限字符串的权限控制
        simpleAuthorizationInfo.addStringPermission("user:*:01");
        simpleAuthorizationInfo.addStringPermission("product:create");
        return simpleAuthorizationInfo;
    }

权限检查部分:

        if(subject.isAuthenticated()){
            //单一角色情况
            System.out.println(subject.hasRole("admin"));
            //多个角色情况,该方法返回一个Boolean list,对应不同角色的True或者False
            System.out.println(subject.hasRoles(Arrays.asList("admin", "user")));
            //多个角色同时满足情况,全部满足返回True,任意一个不满足则返回False
            System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));
            //基于权限字符串
            System.out.println(subject.isPermitted("user:create"));//false
            System.out.println(subject.isPermitted("product:create:001"));//true
        }

5. SpringBoot整合Shiro实战

5.1 快速开始

  • 创建SpringBoot工程,导入相关依赖

     		
            <dependency>
                <groupId>javax.servletgroupId>
                <artifactId>javax.servlet-apiartifactId>
                <scope>providedscope>
            dependency>
            
            <dependency>
                <groupId>javax.servletgroupId>
                <artifactId>jstlartifactId>
            dependency>
            
            <dependency>
                <groupId>org.apache.tomcat.embedgroupId>
                <artifactId>tomcat-embed-jasperartifactId>
                <scope>providedscope>
            dependency>
            
            <dependency>
                <groupId>org.apache.shirogroupId>
                <artifactId>shiro-spring-boot-starterartifactId>
            dependency>
    
  • 配置application.properties

    server.port=8888
    server.servlet.context-path=/shiro
    spring.application.name=shiro
    
    spring.mvc.view.prefix=/
    spring.mvc.view.suffix=.jsp
    
    
  • 在main目录下新建webapp目录,创建index.jsp文件以及login.jsp文件

    
    
    
        
        
        
        Document
    
    
        

    hello world

    
    <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
    
    
        
        
        
        Document
    
    
    登录
    
    用户: 密码:
  • 在项目configuration–>Environment下设置working directory为$MODULE_WORKING_DIR$

  • 启动Spring Boot 工程,在浏览器键入地址:localhost:8888/shiro/index.jsp访问页面

5.2 配置ShiroConfig类

在im.hwp路径下创建config文件夹,创建ShiroConfig类

package im.hwp.config;
import im.hwp.shiro.CustomerRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean getShiroFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        //1.创建ShiroFilterFactoryBean
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //2.为shiroFactoryBean对象绑定SecurityManager,在web项目中,需要绑定defaultWebSecurityManager对象
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        //3. 配置系统公共资源和系统受限资源
        Map<String,String> map = new HashMap<String,String>();
        map.put("/login","anon");//表示公共资源,无需认证即可访问
        //map.put("/index.jsp","authc");
        map.put("/**","authc");//这种表达方式表明所有的资源都需要认证
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        //4.设置web的登录页面,当需要认证和授权时,自动跳转该页面
        shiroFilterFactoryBean.setLoginUrl("login.jsp");
        return shiroFilterFactoryBean;
    }
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;

    }
    @Bean
    public Realm getRealm(){
        return new CustomerRealm();
    }
}

自定义Realm,继承AuthorizingRealm类,实现两个方法,分别获取AuthenticationInfoAuthorizationInfo

package im.hwp.shiro;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class CustomerRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }
}

5.3 创建UserController,实现用户登录和用户退出功能

package im.hwp.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("user")
public class UserController {
    @RequestMapping("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";
    }
    @RequestMapping("logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return  "redirect:/login.jsp";
    }
}

5.4 MD5 + Salt 实现用户注册

  • 导入相关坐标

    
            <dependency>
                <groupId>org.mybatis.spring.bootgroupId>
                <artifactId>mybatis-spring-boot-starterartifactId>
                <version>2.1.2version>
            dependency>
            
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>5.1.38version>
            dependency>
    		
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>druidartifactId>
                <version>1.1.19version>
            dependency>
    
  • 创建数据库Usr表

  • 配置数据库信息

    # 数据源相关配置
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=123
    
    # mybatis相关配置
    mybatis.type-aliases-package=im.hwp.entity
    mybatis.mapper-locations=classpath:im/hwp/mapper/*.xml
    
  • 创建User实体类,UserDao接口,UserDaoMapper.xml, UserService接口和UserServiceImpl类

    package im.hwp.entity;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.experimental.Accessors;
    
    @Data  
    // @Data注解是lombok提供的,可以为类提供get和set方法, 还有 equals()、hashCode()、toString() 、isXxx()等方法
    @Accessors(chain = true)
    // @Accessors用于配置getter和setter方法的生成结果
    @AllArgsConstructor//生成全参数构造函数
    @NoArgsConstructor//生成无参构造函数
    public class User {
        private String username;
        private String password;
        private String id;
        private String salt;
    }
    

    UserDao

    package im.hwp.dao;
    
    import im.hwp.entity.User;
    import org.apache.ibatis.annotations.Mapper;
    
    
    @Mapper
    public interface UserDao {
        void save(User user);
    }
    

    UserDaoMapper

    
    
    <mapper namespace="im.hwp.dao.UserDao">
        <insert id="save" parameterType="User" keyProperty="id" useGeneratedKeys="true">
            insert into usr values (#{id},#{username},#{password},#{salt})
        insert>
    mapper>
    
    

    UserService

    package im.hwp.service;
    
    import im.hwp.entity.User;
    public interface UserService {
        void register(User user);
    }
    

    UserServiceImpl

    package im.hwp.service.impl;
    
    import im.hwp.dao.UserDao;
    import im.hwp.entity.User;
    import im.hwp.service.UserService;
    import org.apache.shiro.crypto.hash.Md5Hash;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.UUID;
    
    @Service
    @Transactional
    public class UserServiceImpl implements UserService {
        @Autowired
        private UserDao userDao;
    
        @Override
        public void register(User user) {
            //对用户明文密码进行md5操作
            String salt = UUID.randomUUID().toString().replaceAll("-", "");
            Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
            String newPassWd = md5Hash.toHex();
            user.setPassword(newPassWd);
            user.setSalt(salt);
            userDao.save(user);
        }
    }
    

    搭建前端注册页面

    
    <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
    
    
        
        
        
        Document
    
    
    
    用户名
    密码

    在前端请求到后端Controller自动封装的过程中,要求前端form表单中input的name在后端实体类中存在对应属性且有getter和setter方法

5.5 MD5 + Salt 认证开发

UserDao根据用户名查询用户信息方法

package im.hwp.dao;
import im.hwp.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface
UserDao {
    void save(User user);
    User findByUsername(String username);
}

UserMapper.xml增加查询语句,注意该方法中id的名称要与UserDao接口方法的方法名一致

    <select id="findByUsername" parameterType="String" resultType="User">
        select username,password,salt from usr where username = #{username}
    select>

UserService和UserServiceImpl中增加相关查询方法

    @Override
    public User findUser(String username) {
        User user = userDao.findByUsername(username);
        return user;
    }

自定义Realm类中重写doGetAuthenticationInfo方法实现加密密码的登录验证

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("用户认证程序执行");
        String principal = (String) authenticationToken.getPrincipal();
        //根据获取的用户名去数据库中获取对应的加密后密码和随机盐
        User user = userService.findUser(principal);
        if(user !=null){
            //封装信息有 用户名 密码 随机盐 和 当前对象
            return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
        }
        return null;
    }

ShiroConfig类中为自定义的Realm类绑定凭证匹配器,指定加密方法哈希散列次数

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean getShiroFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        //1.创建ShiroFilterFactoryBean
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //2.为shiroFactoryBean对象绑定SecurityManager,在web项目中,需要绑定defaultWebSecurityManager对象
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        //3. 配置系统公共资源和系统受限资源
        Map<String,String> map = new HashMap<String,String>();
        map.put("/user/login","anon");
        map.put("/login.jsp","anon");
        map.put("/user/register","anon");
        map.put("/register.jsp","anon");
        map.put("/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        //4.设置web的登录页面,当需要认证和授权时,自动跳转该页面
        shiroFilterFactoryBean.setLoginUrl("login.jsp");
        return shiroFilterFactoryBean;
    }
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }
    @Bean
    public Realm getRealm(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        hashedCredentialsMatcher.setHashIterations(1024);
        CustomerRealm customerRealm = new CustomerRealm();
        customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        return customerRealm;
    }
}

5.6 常用注解的总结

  • @Bean

    1. Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。

    2. SpringIOC 容器管理一个或者多个bean,这些bean都需要在@Configuration注解下进行创建,在一个方法上使用@Bean注解就表明这个方法需要交给Spring进行管理

      @Bean相当于Spring xml中****

  • @Configuration

    1. @Configuration注解标识的类中声明了1个或者多个@Bean方法,Spring容器可以使用这些方法来注入Bean。

    2. @Configuration可以跟@Profile一起使用,说明只有在给定的profile下@Configuration 才能生效。

    3. @Configuration可以和@Import注解一起使用,表明引入其他的配置类。

  • @RequestMapping

    RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。

    @RequestMapping六大属性:

    1. value:指定请求的实际地址。

      @RequestMapping("/login") 等价于 @RequestMapping(value = "/login")
      @RequestMapping(value = {"/login","signin"}//表示请求地址满足两者中任意一个即可
      
    2. method: 用于指定请求的方法,可以设置单个或多个,满足其中任意一个即可。

      @RequestMapping(value = "login", method = {RequestMethod.GET,RequestMethod.POST})
      
    3. headers: 指定request中必须包含某些指定的header值

      @RequestMapping(value = "login", method = RequestMethod.POST, headers="Referer=http://www.hi.com/")
      
    4. params: 指定request中必须包含某些参数值

      @RequestMapping(value = "login", method = RequestMethod.POST, params="user=admin")
      
    5. consumes: 指定处理请求的提交内容类型(Content-Type)

      @RequestMapping(value = "login", method = RequestMethod.POST, consumes="application/json")
      
    6. produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;

      @RequestMapping(value = "login", method = RequestMethod.POST, produces="application/json")
      

    @Service用于标注业务层组件,@Controller用于标注控制层组件(如struts中的action),@Repository用于标注数据访问组件,即DAO组件,而@Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注

  • @Controller

    控制器类,处理由DispatcherServlet分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Model返回给对应的View进行展示

    Spring Boot中控制层使用注解@Controller时,配合@RequestMapping(@GetMapping, @PostMapping)注解的方法返回值对应的是一个视图。

    使用@RestController返回值对应的是json数据,而**@Controller+@ResponseBody**的作用相当于@RestController。

  • @Service

    业务层注解,在Controller中使用@Autowired注解注入@Service注释的service层接口,可以调用业务上的功能。

  • @Repositoty

    DAO层注解,在service层对应的实现类中可以使用@Autowired注解注入该接口,实现数据库相关操作。

    @Repository 和 @Mapper的区别:

    在使用@Repository注解时,需要在Spring中配置@MapperScan(“im.hwp.dao”),该注解指明对应dao层的包路径

    package im.hwp;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @MapperScan("im.hwp.dao")
    public class SpringBootShiroJspApplication {
        public static void main(String[] args) {
            SpringApplication.run(SpringBootShiroJspApplication.class, args);
        }
    }
    

    而在使用@Mapper注解时,不需要额外的进行路径配置,只需要在对应的mapper.xml文件中的namespace指定对应的dao接口,就可以实现自动注入。

  • @Mapper

    1. @Mapper注解将Dao接口交给Spring进行管理;

    2. 使用该注解以后不需要再写Mapper.xml映射文件;

    3. 为该Dao接口生成一个实现类,让别的类使用;

    注意事项:

    ​ 1. 接口内不能出现重名的方法

    ​ 2. 方法参数有多个的时候,需要使用@Param

  • @Component

    通用的一个注解,当不知道如何对类进行归类时可以使用该注解。

5.7 实现用户授权

用户授权的两种方式: 角色权限字符串

image-20201027094447240

由于用户与角色角色与权限均为多对多的关系,因此需要引入用户-角色表角色-权限表将关系改成一对多

  1. 除初始的usr表以外,创建其余四张表。由于只是简单的demo,所以在创建时并没有引入外键等约束。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission`  (
  `pid` int(6) NOT NULL,
  `p_name` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  `url` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
  PRIMARY KEY (`pid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `rid` int(6) NOT NULL,
  `role_name` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  PRIMARY KEY (`rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission`  (
  `id` int(6) NOT NULL,
  `rid` int(6) NOT NULL,
  `pid` int(6) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Table structure for usr
-- ----------------------------
DROP TABLE IF EXISTS `usr`;
CREATE TABLE `usr`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(40) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  `password` varchar(40) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  `salt` varchar(40) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Table structure for usr_role
-- ----------------------------
DROP TABLE IF EXISTS `usr_role`;
CREATE TABLE `usr_role`  (
  `id` int(6) NOT NULL,
  `uid` int(6) NOT NULL,
  `rid` int(6) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Compact;

SET FOREIGN_KEY_CHECKS = 1;
  1. 创建角色实体,权限实体,在用户实体中追加角色集合

    @Data
    @Accessors(chain = true)
    @AllArgsConstructor
    @NoArgsConstructor
    public class Role {
        private String id;
        private String name;
    }
    
    @Data
    @Accessors(chain = true)
    @AllArgsConstructor
    @NoArgsConstructor
    public class Perms {
       private String id;
       private String name;
       private String url;
    }
    
    @Data
    @Accessors(chain = true)
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private String username;
        private String password;
        private String id;
        private String salt;
        private List<Role> roles;
    }
    
  2. 创建xml查询实现,添加UserDao接口方法,UserService接口方法和对应实现类方法

        <resultMap id="userMap" type="User">
            <id column="uid" property="id"/>
            <result column="username" property="username"/>
            <collection property="roles" javaType="list" ofType="Role">
                <id column="rid" property="id"/>
                <result column="role_name" property="name"/>
            collection>
        resultMap>
        <select id="findRoleByUsername" parameterType="String" resultMap="userMap">
            SELECT usr.id uid, username, role.rid rid, role_name
            from usr
            LEFT JOIN usr_role
            on usr.id = usr_role.uid
            LEFT JOIN role
            on role.rid = usr_role.rid
            where username = #{username}
        select>
    
    @Mapper
    public interface
    UserDao {
        void save(User user);
        User findByUsername(String username);
        User findRoleByUsername(String username);
    }
    
    public interface UserService {
        void register(User user);
        User findUser(String username);
        List<Role> findRoleByUsername(String username);
    }
    
    @Service
    @Transactional
    public class UserServiceImpl implements UserService {
        @Autowired
        private UserDao userDao;
    
        @Override
        public void register(User user) {
            //对用户明文密码进行md5操作
            String salt = UUID.randomUUID().toString().replaceAll("-", "");
            Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
            String newPassWd = md5Hash.toHex();
            user.setPassword(newPassWd);
            user.setSalt(salt);
            userDao.save(user);
        }
    
        @Override
        public User findUser(String username) {
            User user = userDao.findByUsername(username);
            return user;
        }
        
        @Override
        public List<Role> findRoleByUsername(String username) {
            User user = userDao.findRoleByUsername(username);
            return user.getRoles();
        }
    }
    
  3. 在自定义Realm的doGetAuthorizationInfo方法中实现授权信息的获取和封装

        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
            System.out.println("==========用户授权开始执行,当前用户为:" + primaryPrincipal + "============");
            List<Role> roles = userService.findRoleByUsername(primaryPrincipal);
            if(!CollectionUtils.isEmpty(roles)){
                SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
                roles.forEach(role -> {
                    simpleAuthorizationInfo.addRole(role.getName());
                    //同理,simpleAuthorizationInfo对象还可以添加权限字符串
                    //simpleAuthorizationInfo.addStringPermission("user:*:*");
                });
                return simpleAuthorizationInfo;
            }
            return null;
        }
    
  4. 权限检查的实现方式

    • 在Controller里面使用代码进行判断用户是否具有对应的权限

         @RequestMapping("update")
          public String updateInfo(){
              try{
                  Subject subject = SecurityUtils.getSubject();
                  if (subject.hasRole("admin")){
                      System.out.println("用户具有权限");
                  }else{
                      System.out.println("用户不具有该权限");
                  }
              }catch (Exception e){
                  System.out.println(e);
              }
              return "redirect:/index.jsp";
          }
      
    • 在Controller请求接口入口处添加权限要求

          @RequestMapping("update")
          @RequiresPermissions("user:update:*") //权限字符串方式
      	//@RequiresRoles("admin") //角色方式
          public String updateInfo(){
              try{
                  System.out.println("用户获得权限执行");
              }catch (Exception e){
                  System.out.println(e);
              }
              return "redirect:/index.jsp";
          }
      

      注意,在没有给ShiroConfig类配置以下参数的时候,上面的角色和权限检查不会起作用。

          @Bean
          public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
              DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
              advisorAutoProxyCreator.setProxyTargetClass(true);
              return advisorAutoProxyCreator;
          }
      
          @Bean
          public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
              AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
              authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager);
              return authorizationAttributeSourceAdvisor;
          }
      
    • 在前端代码中添加权限

          
        • 修改信息
        • 删除信息
        • 添加信息
      • 商品管理
      • 价格管理

5.8 Shiro实现EhCache缓存

​ 从前面的开发过程可以得知,Shiro框架通过Realm获取用户存储在数据库的对应的角色和权限,而每次刷新或者请求,都会导致数据库的查询。很明显,对于角色和权限字符串这种更新频率不是非常高的数据而言,每次的数据库查询带来的开销非常大。因此需要引入**缓存机制,减少数据库的IO**。

  • 引入EhCache坐标

            
            <dependency>
                <groupId>org.apache.shirogroupId>
                <artifactId>shiro-ehcacheartifactId>
                <version>1.5.3version>
            dependency>
    
  • 在ShiroConfig中创建自定义Realm时,设置缓存管理器,并开启缓存管理。

        @Bean
        public Realm getRealm(){
            HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
            hashedCredentialsMatcher.setHashAlgorithmName("MD5");
            hashedCredentialsMatcher.setHashIterations(1024);
            CustomerRealm customerRealm = new CustomerRealm();
            customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
            //设置缓存管理器
            customerRealm.setCacheManager(new EhCacheManager());
            //开启缓存管理
            customerRealm.setCachingEnabled(true);
            customerRealm.setAuthenticationCachingEnabled(true);
            customerRealm.setAuthorizationCachingEnabled(true);
            customerRealm.setAuthenticationCacheName("authenticationCache");
            customerRealm.setAuthorizationCacheName("authorizationCache");
            return customerRealm;
        }
    

    设置完成以后,在**用户每次登录时,会执行sql查询获取用户的角色和权限信息,刷新页面和跳转页面则不会执行sql查询而是直接从缓存**中获取。

5.9 Shiro整合Redis实现缓存

  1. Spring Boot整合Redis坐标

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
  2. 在springboot配置文件中配置redis

    # redis相关配置
    spring.redis.port=6379
    spring.redis.host=localhost
    spring.redis.database=0
    
  3. 修改所有的实体类,实现Serializable接口

  4. 自定义RedisCacheManager

    在设计RedisCacheManager时,参考EhCacheManager,其实现了CacheManager接口,因此在自定义的RedisCacheManager类时需要实现该接口。

    package im.hwp.shiro.cache;
    
    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.cache.CacheException;
    import org.apache.shiro.cache.CacheManager;
    
    public class RedisCacheManager implements CacheManager {
        @Override
        public <K, V> Cache<K, V> getCache(String s) throws CacheException {
            System.out.println("RedisCacheManager.getCache被调用:" + s);
            return new RedisCache<K,V>(s);
        }
    }
    
  5. 自定义RedisCache,实现Cache接口

    注意:

    1. 在RedisCache中实现了与Redis交互的相关方法,但是在该类中不能直接使用@AutoWired注解自动注入RedisTemplate,因为该类没有交给Bean容器管理。所以需要写一个工具类,通过ApplicationContext获取这个Bean。

      package im.hwp.utils;
      
      import org.springframework.beans.BeansException;
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.ApplicationContextAware;
      import org.springframework.stereotype.Component;
      
      @Component
      public class ApplicationContextUtils implements ApplicationContextAware {
          private static ApplicationContext context;
          @Override
          public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
              System.out.println("setApplicationContext 方法执行了");
              this.context = applicationContext;
              System.out.println(context);
          }
          //通过ApplicationContext获取对应的bean
          public static Object getBean(String beanName){
              return context.getBean(beanName);
          }
      }
      
    2. 在使用Redis进行缓存管理时,需要进行序列化和反序列化。而在Shiro中有个Bug,即在用户认证方法中创建SimpleAuthenticationInfo对象时,对盐进行转换时不可以使用Shiro自己的ByteSource。因为该接口中方法创建的SimpleByteSource对象没有无参构造器,其无法进行反序列化。

      package im.hwp.utils;
      
      import org.apache.shiro.codec.Base64;
      import org.apache.shiro.codec.CodecSupport;
      import org.apache.shiro.codec.Hex;
      import org.apache.shiro.util.ByteSource;
      
      import java.io.File;
      import java.io.InputStream;
      import java.io.Serializable;
      import java.util.Arrays;
      
      public class MyByteSource implements ByteSource, Serializable {
      
          /*这里将final去掉了,去掉后要在后面用getter和setter赋、取值*/
          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;
          }
      
      
          /*这里加了getter和setter*/
          public void setBytes(byte[] bytes) {
              this.bytes = bytes;
          }
      
          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);
              }
          }
      
          /*取代原先加盐的工具类*/
          public static class Util{
              public static ByteSource bytes(byte[] bytes){
                  return new MyByteSource(bytes);
              }
      
              public static ByteSource bytes(String arg0){
                  return new MyByteSource(arg0);
              }
          }
      }
      

    实现RedisCache

    package im.hwp.shiro.cache;
    
    import im.hwp.utils.ApplicationContextUtils;
    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.cache.CacheException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    import org.springframework.stereotype.Component;
    
    import java.util.Collection;
    import java.util.Set;
    
    @Component
    public class RedisCache<k,v> implements Cache<k,v> {
        private String cacheName;
    
        public RedisCache(String cacheName) {
            this.cacheName = cacheName;
        }
        public RedisCache() {
        }
    
        @Override
        public v get(k k) throws CacheException {
            RedisTemplate redisTemplate = getRedistemplate();
            return (v) redisTemplate.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);
            RedisTemplate redisTemplate = getRedistemplate();
            redisTemplate.opsForHash().put(this.cacheName, k.toString(), v);
            return null;
        }
    
        @Override
        public v remove(k k) throws CacheException {
            return null;
        }
    
        @Override
        public void clear() throws CacheException {
    
        }
    
        @Override
        public int size() {
            return 0;
        }
    
        @Override
        public Set<k> keys() {
            return null;
        }
    
        @Override
        public Collection<v> values() {
            return null;
        }
        
        private RedisTemplate getRedistemplate(){
            RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            return redisTemplate;
        }
    }
    
    
  6. 在ShiroConfig中获取Realm时,对Realm设置Redis缓存管理器

    @Bean
        public Realm getRealm(){
            HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
            hashedCredentialsMatcher.setHashAlgorithmName("MD5");
            hashedCredentialsMatcher.setHashIterations(1024);
            CustomerRealm customerRealm = new CustomerRealm();
            customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
            //设置缓存管理器
    //        customerRealm.setCacheManager(new EhCacheManager());
            customerRealm.setCacheManager(new RedisCacheManager());
            //开启缓存管理
            customerRealm.setCachingEnabled(true);
            customerRealm.setAuthenticationCachingEnabled(true);
            customerRealm.setAuthorizationCachingEnabled(true);
            customerRealm.setAuthenticationCacheName("authenticationCache");
            customerRealm.setAuthorizationCacheName("authorizationCache");
            return customerRealm;
        }
    

5.10 整合图片验证码功能

图片验证码工具类:

package im.hwp.utils;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;

import javax.imageio.ImageIO;

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);
        }
    }
}

前端页面添加验证码相关内容:

<form action="${pageContext.request.contextPath}/user/login" method="post">
    用户:<input name="username" type="text">
    密码:<input name="password" type="password">
    <input type="text" name="verifyCode"><img src="${pageContext.request.contextPath}/user/getImage" >
    <input type="submit" value="登录">
form>

在控制器中添加获取图片验证码的方法:

    @RequestMapping("getImage")
    public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
        //生成验证码
        String code = VerifyCodeUtils.generateVerifyCode(4);
        //验证码存入session
        session.setAttribute("code",code);
        //验证码生成图片
        ServletOutputStream os = response.getOutputStream();
        response.setContentType("image/png");
        VerifyCodeUtils.outputImage(220,60,os, code);
    }

修改控制器中登录方法,实现验证码校验:

@RequestMapping(value = "login", method = {RequestMethod.GET,RequestMethod.POST})
    public String login(String username, String password, String verifyCode, HttpSession session){
        //获取主体对象
        String code = (String) session.getAttribute("code");
        System.out.println("用户传上来的code:" + verifyCode + " 系统存储的code:" + code);
        try {
            if(code.equalsIgnoreCase(verifyCode)){
                session.removeAttribute("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){
            System.out.println(e.getMessage());
        }
        return "redirect:/login.jsp";
    }

在ShiroConifg中放行获取验证码的路径:

   //3. 配置系统公共资源和系统受限资源
        map.put("/user/getImage","anon");

到此,图片验证码整合工作完成。

你可能感兴趣的:(shiro,spring,boot,shiro,spring,boot)