认证传送门:https://blog.csdn.net/JavaJieRui/article/details/108465791
授权传送门:https://blog.csdn.net/JavaJieRui/article/details/108495686
继上次说完了Shiro中的授权之后,Shiro中的认证和授权就全部为大家讲解完了,相信大家是跟着前两篇文章看过来的对Shiro在程序中进行认证和授权的操作都已经略知一二了,下面就为大家讲解一下Shiro在SpringBoot中的应用整合,同时在之前我们做的认证授权都是在代码中硬编码直接写死的,而在我们这次与SpringBoot的整合开发中,将会使用数据库中的数据来替代代码中的数据,更加真实的模拟我们日常在工作中对权限控制开发。
首先我们不论是开发任何功能,都是要对需求和思路进行清晰透彻的梳理和理解,虽然我们本次开发是模拟的真实开发场景,我们也要对开发之前的需求,实现的流程进行梳理。下面先为大家展示一张流程图,之后我会通过整张流程图再来讲解Shiro与SpringBoot整合开发的思路。
首先,我们要开发权限第一步是要先有一个SpringBoot的应用,这个我们做权限是对SpringBoot应用中的所有资源进行保护,如图所示,一个页面可以是一个被保护的资源。同样,程序中的一个方法也可以是一个被保护的资源,但是在一个SpringBoot的应用中,不仅仅要有被保护的资源,还要有公共资源,因为我们如果没有公共资源,登录或者注册的页面就没有办法被用户访问到,那假如说连注册和登录都是被保护起来的资源,那我们这个系统又怎么用呢?
所以门将所有的系统资源分为了两大类,一类是公共资源例如:登录页、登录的方法、注册页、注册的方法、可能还会有找回密码等等的这些统一划分为公共资源,而例如主页,或对系统内部数据的增删改查等的操作就被划分为受限资源,此时这些资源就要被我们用Shiro做的权限验证来保护起来。
前面我们说了那么多怎么把系统分为受限资源,公共资源,怎么用Shiro对他们进行权限的保护,但这些都有一个大的前提,必须让Shiro拿到SpringBoot应用中的所有的请求,为什么呢?应用中的所有资源都是通过客户通过一个又一个的请求来访问的,假如说我们的Shiro连请求都拿不到,又何谈划分资源何谈保护资源呢,所以所有的请求过来,首先要经过一个过滤器。对请求分类,看是受限资源还是公共资源,如果是公共资源不需要认证则直接放行,如果是受限资源呢?受限资源受限经过SecurityManager这个类进行认证处理,从我们的自定义Realms中拿到数据,同时Realms中还会有一系列的密码凭证匹配器,三列次数,算法名称等等一系列的操作。认证通过之后还要进行授权的操作,也就是说虽然你通过了认证,但是我们的Shiro还要来看看你是否有操作这个资源的权限,这样也就达到了相对细粒度的权限控制。
好了经过上面的一张流程图的展示以及对图的解释,我相信大家已经对流程有了相应的了解,下面就可以进行实战开发了。
首先上项目目录结构
POM文件<这时还没有引入Shiro>
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.4.RELEASE
com.csdn
springboot_shiro_jsp
0.0.1-SNAPSHOT
springboot_shiro_jsp
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.apache.tomcat.embed
tomcat-embed-jasper
jstl
jstl
1.2
org.springframework.boot
spring-boot-maven-plugin
application.properties
#指定端口号
server.port=8090
#请求路径中加上项目名
server.servlet.context-path=/shiro
#指定项目名
spring.application.name=shiro
#将mvc视图改为jsp,前缀、后缀
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
在新建的webapp中新建一个index.jsp:
在IDEA中设置它的工作目录:
接下来启动,看一下效果:
好的,这样基本的springboot的一个应用就搭建起来了。
Shiro与Springboot整合首先在项目目录中新建一个config包用来放项目中的各种配置类,之后编写一个类名叫ShiroConfig表明是Shiro的配置类,因为以后再项目中也许会放很多配置类,所以要见名知意表明是shiro的配置类。首先我们先将ShiroConfig的架子搭起来,具体代码如下:
/**
* 用来整合shiro与springboot的配置类 关系就是一层依赖一层
*/
@Configuration
public class ShiroConfig {
//创建ShiroFilter过滤器 负责拦截所有请求
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给过滤器设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}
//创建安全管理器
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = newDefaultWebSecurityManager(realm);
//给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//创建自定义Realm
@Bean
public Realm realm(){
MyRealm myRealm = new MyRealm();
return myRealm;
}
}
从上面的代码中可以看到最后一个为自定义Realm,那么我们也来新建一个名叫ralms的包。用来存放我们的自定义Realm:
/*
* 自定义Realm
*/
public class MyRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
那么现在一个简单的架子我们就搭起来了,下面来完成具体实现。
那么虽然我们现在已经将架子搭建起来了,但是我们并没有在shiroconfig中写出来那些请求需要认证那些请求直接放行,所以所有的页面都能被访问到,现在就来看看具体实现。
首先来看的是配置ShiroFilterFactoryBean配置受限资源和登录页路径:
//创建ShiroFilter过滤器 负责拦截所有请求
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = shiroFilterFactoryBean(defaultWebSecurityManager);
//声明一个map
Map map = new HashMap<>();
//配置系统受限资源
map.put("/index.jsp","authc"); //authc: 代表请求这个资源需要认证和授权
//默认的登录页面路径
shiroFilterFactoryBean.setLoginUrl("login.jsp"); //因为shiro与jsp的集成相对友好,所以这个路径如果不写也会默认去找,但也可以改变
//将声明路径的map设置给过滤器
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//给过滤器设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}
如上代码所示,如果配置了页面路径+autch,则视为此页面路径为受保护的资源页面,如果访问这个页面需要认证和授权,并且如果在同级目录下有一个login.jsp的页面会重定向到login.jsp并不需要配置,同时,如果你的login.jsp页面不在统计目录下或者叫其他名字,你也可以通过setLoginUrl的方式来设置你的登录页面来进行跳转。
那么authc这个在map中的value是什么呢?实际上这个authc是shiro为我们提供的一个过滤器的简称,其实还有很多简称下面为大家贴一张图:
虽然有很多过滤器但是最常用的也就是anon和authc这两种,如果日后想用其他的过滤器只需要根据自己的不同需求来选择不同功能的过滤器即可。
接着我们再来编写一个简单的登录页面:
接着来编写一个controller:
@RequestMapping("/user")
@Controller
public class UserController {
/**
* 处理身份认证
* @param username
* @param password
* @return
*/
@RequestMapping("/login")
public String login(String username,String password){
//获取主体对象
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
subject.login(token);
return "redirect:/index.jsp";
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
}
return "redirect:/login.jsp";
}
此时我们编写完毕这个controller之后他一定会走我们的自定义Realm,但是此时我们的自定义Realm并没有写任何的具体实现所以不论我们怎么访问他都会重定向至登录页面:
那暂时我们写再将认证信息写死在代码中,测试一下:
/*
* 自定义Realm
*/
public class MyRealm extends AuthorizingRealm {
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
if (principal.equals("zhangsan")){
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
此时我将认证的信息固定为用户名:zhangsan 密码:123 再来测试一下:
效果:
进入了被保护的资源页面,系统主页。
那么此时还有一个问题,那以后我们所设计的系统一定不会只有这两个页面,一定会有很多被保护的页面,如果挨个去设置会跟麻烦,那么我们怎么办呢?这样的话就可以将一些比较少的公共资源设置为放行,将其他所有的页面设置为 /**,来将其保护起来。但一定要切记,公共资源的代码顺序一定是在被保护的资源的代码上方。
如下修改ShiroConfig:
/**
* 用来整合shiro与springboot的配置类 关系就是一层依赖一层
*/
@Configuration
public class ShiroConfig {
//创建ShiroFilter过滤器 负责拦截所有请求
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//声明一个map
Map map = new HashMap<>();
//配置公共资源
map.put("/login.jsp","anon"); //anon:代表这个资源为公共资源
//配置系统受限资源
map.put("/**","authc"); //authc: 代表请求这个资源需要认证和授权
//将路径设置给过滤器
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//默认的登录页面路径
shiroFilterFactoryBean.setLoginUrl("login.jsp"); //因为shiro与jsp的集成相对友好,所以这个路径如果不写也会默认去找,但也可以改变
//给过滤器设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}
//创建安全管理器
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(realm);
//给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//创建自定义Realm
@Bean
public Realm realm(){
MyRealm myRealm = new MyRealm();
return myRealm;
}
}
效果我就不展示了,与上面一样。
退出登录功能,首先将页面完善:
然后在UserController中编写退出的功能:
/**
* 用户登出功能
* @return
*/
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
try {
subject.logout();
return "redirect:/login.jsp";
} catch (Exception e) {
e.printStackTrace();
return "redirect:/index.jsp";
}
}
shiro有一个好处就是如果你重启的话他也会在缓存中缓存你的登录信息,所以退出功能就是将之前的缓存信息清除掉,之后再访问资源页面时就退重定向到登录的页面。
那么本篇文章要讲的是shiro与springboot的整合开发模拟真实的开发场景,也就是说在真实的开发中我们一定不可能将用户的信息写死在代码中,一定是需要去数据库中获取用户的认证数据来实现认证,那么下面就为大家进一步的整合。
首先,连接数据库进行认证,我们需要有一个注册的页面,实现用户注册,同时我们要在用户注册的时候所填写的明文密码在保存数据库之前使用shiro提供的md5加密算法、并且加盐处理之后才能保存到数据库中。只有这样才能在之后进行认证的时候读出用户的加密密码。
先来编写一个简单的注册页面:
此时用户注册时需要涉及到数据库的保存那么我们就来创建一个数据库,新建一张表来保存用户名以及密码。
id字段自增,然后分别有三个字段用户名、密码以及盐的信息,之前我们说过加在密码中的盐一定要保存到数据库中去,只有这样才能保证我们在认证的时候加入相同的盐保证用户认证信息的通过。
既然我们需要用到随机盐那么我们就需要用到一个盐工具类那么我们在工程下的utils包中先来编写一个盐工具类:
/**
* 随机盐工具类
*/
public class SaltUtils {
public String getSalt(int n) {
//首先定义一个char数组
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
//每次拿到一个随机字符
char aChar = chars[new Random().nextInt(chars.length)];
//拼接随机字符
sb.append(aChar);
}
return sb.toString();
}
/**
* 测试随机盐工具类
* @param args
*/
public static void main(String[] args) {
SaltUtils saltUtils = new SaltUtils();
String salt = saltUtils.getSalt(8);
System.out.println(salt);
}
}
修改之后的POM文件:
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.4.RELEASE
com.csdn
springboot_shiro_jsp
0.0.1-SNAPSHOT
springboot_shiro_jsp
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.apache.tomcat.embed
tomcat-embed-jasper
jstl
jstl
1.2
org.apache.shiro
shiro-spring-boot-starter
1.6.0
mysql
mysql-connector-java
5.1.49
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.3
com.alibaba
druid
1.2.1
org.springframework.boot
spring-boot-maven-plugin
2.3.4.RELEASE
新建一个实体类:
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private String id;
private String username;
private String password;
private String salt;
}
接着对项目的application.properties文件进行修改:
#指定端口号
server.port=8090
#请求路径中加上项目名
server.servlet.context-path=/shiro
#指定项目名
spring.application.name=shiro
#将mvc视图改为jsp,前缀、后缀
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
#数据源相关配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#数据源类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath:com/csdn/mappers/*.xml
相关配置写完后就可以来编写注册的方法并将用户的数据保存在数据库中。
dao层代码:
@Mapper
public interface UserDao {
/**
* 用户注册,保存
* @param user
*/
void save(User user);
}
service层代码:
public interface UserService {
/**
* 用户注册保存信息
* @param user
*/
void save(User user);
}
serviceImpl层代码:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* 用户注册保存用户信息
* @param user
*/
@Override
public void save(User user) {
//获取随机盐
String salt = new SaltUtils().getSalt(8);
//拿到用户输入的密码
String password = user.getPassword();
//新建一个md5Hash的对象传参
Md5Hash md5Hash = new Md5Hash(password,salt,1024);
//s 即为处理后的密码
String s = md5Hash.toHex();
//保存
user.setPassword(s);
user.setSalt(salt);
userDao.save(user);
}
}
上面业务层的代码主要是对密码的处理,一方面是获取随机盐,然后保存随机盐,另一方面则是将输入的密码获取到之后进行加密处理后再将其密码进行保存。
controller层代码:
/**
* 用户注册
* @param user
* @return
*/
@RequestMapping("/register")
public String Register(User user){
try {
userService.save(user);
return "redirect:/login.jsp";
} catch (Exception e) {
e.printStackTrace();
return "redirect:/register.jsp";
}
}
}
保存成功后直接重定向到登录页面,如果报错之后则在原页面。
mapper.xml文件:
insert into t_user values(#{id},#{username},#{password},#{salt})
运行过程就不展示了,直接到数据库去查看一下结果,我输入的是:用户名:zhangsan 密码:123456,数据库截图:
现在我们已经将注册之后的用户信息用md5的方式加密并加盐散列1024次之后将密码以及随机盐的内容存入了数据库中,现在只需要将用户信息读取之后以相同的方式解密认证即可。
首先我们先编写dao层因为需要一个根据用户输入的用户名来获取用户信息的方法,所以首先编写一个dao层的代码以及mapper文件:
@Mapper
public interface UserDao {
/**
* 用户注册,保存
* @param user
*/
void save(User user);
/**
* 根据用户名查询用户
* @param username
* @return
*/
User findByUsername(String username);
}
mapper文件修改:
insert into t_user values(#{id},#{username},#{password},#{salt})
业务层代码:
public interface UserService {
/**
* 用户注册保存信息
* @param user
*/
void save(User user);
/**
* 根据用户名查询用户
* @param username
* @return
*/
User findByUsername(String username);
}
业务层实现类
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* 用户注册保存用户信息
* @param user
*/
@Override
public void save(User user) {
//获取随机盐
String salt = new SaltUtils().getSalt(8);
//拿到用户输入的明文密码
String password = user.getPassword();
//新建一个md5Hash的对象传参
Md5Hash md5Hash = new Md5Hash(password,salt,1024);
//s 即为处理后的密码
String s = md5Hash.toHex();
//保存
user.setPassword(s);
user.setSalt(salt);
userDao.save(user);
}
/**
* 根据用户名查询用户
* @param username
* @return
*/
@Override
public User findByUsername(String username) {
User user = userDao.findByUsername(username);
return user;
}
}
根据之前我们的已经了解过得shiro的匹配规则此时,我们还需要去自定义Realm中去修改,从数据库中获取用户信息,所以我们这时要去自定义Realm去修改。
而此时因为我们的用户信息是通过数据库获取的,自定义的Realm又不在工厂中被Spring管理所以此时我们需要一个工具类即为通过类名来获取工厂中的对象。
/**
* 根据bean名字获取工厂中指定的bean对象
*/
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context=applicationContext;
}
public static Object getBean(String beanName){
Object bean = context.getBean(beanName);
return bean;
}
}
之前我们通过@Service标签中声明了一个名字为userService,这样就可以通过对象名字获取工厂中的对象。
之后修改自定义Realm:
/*
* 自定义Realm
*/
public class MyRealm extends AuthorizingRealm {
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
//在工厂中获取service对象
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
User user = userService.findByUsername(principal);
//判断user不为空时处理
if (!ObjectUtils.isEmpty(user)){
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
因为我们将密码加密的策略已经修改为了md5加散列所以要将密码匹配策略修改,修改shiroConfig:
/**
* 用来整合shiro与springboot的配置类 关系就是一层依赖一层
*/
@Configuration
public class ShiroConfig {
//创建ShiroFilter过滤器 负责拦截所有请求
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//声明一个map
Map map = new HashMap<>();
//配置公共资源
map.put("/user/**","anon"); //anon:代表这个资源为公共资源
map.put("/register.jsp","anon");
//配置系统受限资源
map.put("/**","authc"); //authc: 代表请求这个资源需要认证和授权
//将路径设置给过滤器
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//默认的登录页面路径
shiroFilterFactoryBean.setLoginUrl("/login.jsp"); //因为shiro与jsp的集成相对友好,所以这个路径如果不写也会默认去找,但也可以改变
//给过滤器设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}
//创建安全管理器
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(realm);
//给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//创建自定义Realm
@Bean
public Realm realm(){
MyRealm myRealm = new MyRealm();
//修改密码凭证匹配器 修改匹配策略
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//密码加密方式
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//设置散列次数
hashedCredentialsMatcher.setHashIterations(1024);
myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return myRealm;
}
}
全部修改完成之后,启动项目再来访问一下页面:
成功!
那么springboot整合shiro连接数据库的简单实现就写到这里,后续的文章会继续加上从数据库获取授权信息的简单实现,希望大家共同学习一起成长。