在此首先感谢**编程不良人**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.shiro</groupId><artifactId>shiro-core</artifactId><version>1.5.3</version></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对象的RealmdefaultSecurityManager.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){//在认证过程中,如果用户名不存在,会抛出UnknownAccountExceptionexception.printStackTrace();System.out.println("认证失败:用户名不存在");}catch(IncorrectCredentialsException exception){//认证过程中,如果用户名正常而密码错误,会抛出IncorrectCredentialsExceptionexception.printStackTrace();System.out.println("认证失败:密码错误");}}
}

3.2.2 认证流程的源码分析

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

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {//将token抢转为UsernamePasswordTokenUsernamePasswordToken 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对象的RealmdefaultSecurityManager.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(){//有权限
    }
    
  • 标签式

    <shiro:hasRole name="admin"><!- 有权限 ->
    </shiro:hasRole>
    

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或者FalseSystem.out.println(subject.hasRoles(Arrays.asList("admin", "user")));//多个角色同时满足情况,全部满足返回True,任意一个不满足则返回FalseSystem.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));//基于权限字符串System.out.println(subject.isPermitted("user:create"));//falseSystem.out.println(subject.isPermitted("product:create:001"));//true}

5. SpringBoot整合Shiro实战

5.1 快速开始

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

             <!-- 添加servlet依赖模块 --><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><scope>provided</scope></dependency><!-- 添加jstl标签库依赖模块 --><dependency><groupId>javax.servlet</groupId><artifactId>jstl</artifactId></dependency><!-- springboot内置tomcat没有此依赖 --><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId><scope>provided</scope></dependency><!--引入shir整合springboot依赖--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-starter</artifactId></dependency>
    
  • 配置application.properties

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

    <!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>Document</title>
    </head>
    <body><p>hello world</p>
    </body>
    </html>
    
    <!doctype html>
    <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
    <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>Document</title>
    </head>
    <body>
    登录
    <form action="${pageContext.request.contextPath}/user/login" method="post">用户:<input name="username" type="text">密码:<input name="password" type="password"><input type="submit" value="登录">
    </form>
    </body>
    </html>
    
  • 在项目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 {@Beanpublic ShiroFilterFactoryBean getShiroFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){//1.创建ShiroFilterFactoryBeanShiroFilterFactoryBean 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;}@Beanpublic DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(realm);return defaultWebSecurityManager;}@Beanpublic 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 {@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {return null;}@Overrideprotected 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 实现用户注册

  • 导入相关坐标

    <!--mybatis相关依赖--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.2</version></dependency><!--mysql相关依赖--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.38</version></dependency><!--druid--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.19</version></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

    <?xml version="1.0" encoding="utf-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <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 {@Autowiredprivate UserDao userDao;@Overridepublic 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);}
    }
    

    搭建前端注册页面

    <!doctype html>
    <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
    <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>Document</title>
    </head>
    <body>
    <form action="${pageContext.request.contextPath}/user/register" method="POST">用户名<input type="text" id="username" name="username"></br>密码<input type="password" id="password" name="password"><input type="submit" value="注册">
    </form>
    </body>
    </html>
    

    在前端请求到后端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中增加相关查询方法

    @Overridepublic 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 {@Beanpublic ShiroFilterFactoryBean getShiroFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){//1.创建ShiroFilterFactoryBeanShiroFilterFactoryBean 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;}@Beanpublic DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(realm);return defaultWebSecurityManager;}@Beanpublic 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_namefrom usrLEFT JOIN usr_roleon usr.id = usr_role.uidLEFT JOIN roleon role.rid = usr_role.ridwhere 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 {@Autowiredprivate UserDao userDao;@Overridepublic 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);}@Overridepublic User findUser(String username) {User user = userDao.findByUsername(username);return user;}@Overridepublic 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类配置以下参数的时候,上面的角色和权限检查不会起作用。

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

          <ul><shiro:hasAnyRoles name="user,admin"><ul><shiro:hasPermission name="user:update:*"><li>修改信息</li></shiro:hasPermission><shiro:hasPermission name="user:delete:*"><li>删除信息</li></shiro:hasPermission><shiro:hasPermission name="user:add:*"><li>添加信息</li></shiro:hasPermission></ul></shiro:hasAnyRoles><shiro:hasRole name="admin"><li>商品管理</li><li>价格管理</li></shiro:hasRole></ul>
      

5.8 Shiro实现EhCache缓存

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

  • 引入EhCache坐标

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

        @Beanpublic 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坐标

    <!--redis-->
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
    </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 {@Overridepublic <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;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {System.out.println("setApplicationContext 方法执行了");this.context = applicationContext;System.out.println(context);}//通过ApplicationContext获取对应的beanpublic 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() {}@Overridepublic v get(k k) throws CacheException {RedisTemplate redisTemplate = getRedistemplate();return (v) redisTemplate.opsForHash().get(this.cacheName, k.toString());}@Overridepublic 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;}@Overridepublic v remove(k k) throws CacheException {return null;}@Overridepublic void clear() throws CacheException {}@Overridepublic int size() {return 0;}@Overridepublic Set<k> keys() {return null;}@Overridepublic 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缓存管理器

    @Beanpublic 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);//验证码存入sessionsession.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");

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

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

  1. Spring Boot 整合 shiro 之盐值加密认证详解(六)

    Spring Boot 整合 shiro 之盐值加密认证详解 概述 不加盐认证 加入密码认证核心代码 修改 CustomRealm 新增获取密文的方法 修改 doGetAuthenticationIn ...

  2. 六、Spring Boot整合Shiro

    六.Spring Boot整合Shiro 6.1.整合思路 6.2.创建spring boot项目 6.3.引入shiro依赖 6.4.配置shiro环境 创建配置类ShiroConfig 1.配置: ...

  3. Spring Boot整合Shiro + Springboot +vue

    目录 02 Spring Boot整合Shiro p1.shiro概述 1 什么是Shiro 2 Shiro核心组件 p2.Shiro实现登录认证 AccountRealm.java QueryWra ...

  4. Spring Boot 整合 Shiro(三)Kaptcha验证码 附源码

    前言 本文是根据上篇<Spring Boot 整合Shiro(二)加密登录与密码加盐处理>进行修改,如有不明白的转上篇文章了解. 1.导入依赖 <!-- https://mvnrep ...

  5. Spring Boot 统一功能处理(用户登录权限效验-拦截器、异常处理、数据格式返回)

    文章目录 1. 统一用户登录权限效验 1.1 最初用户登录权限效验 1.2 Spring AOP 统一用户登录验证 1.3 Spring 拦截器 1.4 练习:登录拦截器 1.5 拦截器实现原理 1. ...

  6. spring boot整合Shiro实现单点登录

    默认情况下,Shiro已经为我们实现了和Cas的集成,我们加入集成的一些配置就ok了 1.加入shiro-cas包 <!-- shiro整合cas单点 --><dependency& ...

  7. spring boot整合shiro继承redis_Springboot+Shiro+redis整合

    1.Shiro是Apache下的一个开源项目,我们称之为Apache Shiro.它是一个很易用与Java项目的的安全框架,提供了认证.授权.加密.会话管理,与spring Security 一样都是 ...

  8. Spring boot整合shiro权限管理

    apache shiro: https://shiro.apache.org/ Apache Shiro是一个强大且易用的Java安全框架,执行身份验证.授权.密码学和会话管理.使用Shiro的易于理 ...

  9. spring boot整合shiro+jjwt

    前言 本篇文章将教大家在 shiro + springBoot 的基础上整合 JJWT (JSON Web Token) JWT JSON Web Token(JWT)是一个非常轻巧的规范.这个规范允 ...

最新文章

  1. Nature | 有机合成的数字化
  2. TeeChart经验总结 10.ZoomScroll
  3. 关于软件项目工作量估算的若干问题
  4. hbase建表,删表,修改,查询(get,scan,布隆过滤器)
  5. html怎么让五张照片并排显示,最考验右脑5张照片,30s内能发现问题都是牛人,PS做不出来...
  6. golang学习之旅(2)- go的数据基本数据类型及变量定义方式
  7. 解决Lync Server前端必备组件Wmf2008R2安装失败
  8. Flink的ConGroup算子介绍
  9. Swift -- 7.5 类型属性,方法
  10. deep deepfm wide 区别_个性化推荐如何满足用户口味?微信看一看的技术这样做
  11. js获取前一天/后一天
  12. nosetest忽略执行指定文件方法
  13. 大数据之Hadoop简介
  14. JDK的安装及环境变量配置
  15. win7定时关机命令_如何让win7操作系统实现定时关机,以防我们忘记电脑关机
  16. 关于chm电子书无法显示网页的解决方
  17. 求职季之你必须要懂的原生JS(中)
  18. 实现动画切换渐进渐出效果
  19. Python全栈-magedu-2018-笔记13
  20. 大概率思维《The House Advantage》

热门文章

  1. 移动硬盘如何分区?易我分区大师帮你搞定!
  2. 计算机培训通讯报道,新员工培训通讯稿3篇
  3. 如何编制项目蓝图汇报材料
  4. 关于STM32空闲中断极限时间
  5. Matlab基础代码教程
  6. P6617 查找 Search (线段树)
  7. Ambari入门及安装
  8. 《程序员》2012年12期精彩内容:2012这一年
  9. java如何写出简洁代码
  10. Linux内存管理(二十六):slub 分配器初始化