shiro和spring security 都是安全框架,都可以授权认证。
对比来讲spring security自定义能力更强点,shiro单纯配置来说更复杂点.
不过我技术不精,还是使用我更熟悉的shiro来进行授权认证功能的实现
下图是shiro的3层构造,而我们写也是根据这三层构造来写的,用户->安全事务管理器->realm对象
1.依赖shiro包
还是在pom文件写入
<!--shiro整合spring-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.1</version>
</dependency>
2.创建config文件和Realm文件
config文件:
@Configuration
public class ShiroConfig {
//shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
//关联SecurityManager
bean.setSecurityManager(securityManager);
return bean;
}
//DefaultwebSecurityManager 配置核心安全事务管理器
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("myRealm") MyRealm myRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//关联Realm
securityManager.setRealm(myRealm);
return securityManager;
}
//创建realm对象(需要自定义)
@Bean
public MyRealm myRealm(@Qualifier("credentialsMatcher") HashedCredentialsMatcher credentialsMatcher){
MyRealm myRealm=new MyRealm();
myRealm.setCredentialsMatcher(credentialsMatcher);
return myRealm;
}
//以上三层是shiro的三层结构
//下面是附加的
// 配置密码比较器(密码加密)
@Bean(name="credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
//RetryLimitHashedCredentialsMatcher为另外类的构造函数
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
credentialsMatcher.setHashIterations(0);//散列的次数,比如散列两次,相当于 md5(md5(""));
return credentialsMatcher;
}
}
Realm文件:
public class MyRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权!!!!!");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证!!!!!");
return null;
}
}
3.完善config文件
config文件:
我们还可以在shiroFilterFactoryBean那一层加配置设置很多东西
//shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
//关联SecurityManager 配置核心安全事务管理器
bean.setSecurityManager(securityManager);
//配置登录的URL(未登录的用户访问的页面)
//bean.setLoginUrl("/auth/login");
// 配置登录成功的url(登录后用户访问的页面)
//bean.setSuccessUrl("/auth/index");
//自定义拦截器
Map<String,Filter> MyfiltersMap=new LinkedHashMap<String,Filter>();
//限制同一个账号的同时在线个数
//MyfiltersMap.put("kickout",kickout)
//配置访问权限
//<url,权限类型>
Map<String,String> filterChainDefinitionMap=new LinkedHashMap<>();
//注意过滤器配置顺序 不能颠倒
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
filterChainDefinitionMap.put("/auth/logout","logout");
// 配置不会被拦截的链接 顺序判断
//authc:所有url都必须认证通过才可以访问;
// anon:所有url都都可以匿名访问
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/img/**","authc");//表示需要认证才可以访问
filterChainDefinitionMap.put("/auth/login","anon");
filterChainDefinitionMap.put("/*", "anon");
filterChainDefinitionMap.put("/**", "anon");
//设置授权形式的访问
filterChainDefinitionMap.put("/sardine/**","perms[admin:play]");
//设置未授权界面
bean.setUnauthorizedUrl("/403");
//配置shiro的主要拦截器(默认有个拦截器)
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
anon: 无需认证即可访问
authc: 需要认证才可访问
user: 点击“记住我”功能可访问
perms: 拥有权限才可以访问
role: 拥有某个角色权限才能访问
4.完善Realm文件之登录验证
前一章偷懒的,这里补上,创建业务层,并且多加个login方法进行登录验证
public interface UsersService {
List<Users> queryUserList();
Users queryUserByUserName(String username);
Users queryUserById(int id);
ResponseBean login(String username,String password);
}
@Service
public class UsersServiceImpl implements UsersService{
@Autowired
private UsersMapper usersMapper;
@Override
public List<Users> queryUserList() {
return usersMapper.queryUserList();
}
@Override
public Users queryUserByUserName(String username) {
return usersMapper.queryUserByUserName(username);
}
@Override
public Users queryUserById(int id) {
return usersMapper.queryUserById(id);
}
@Override
public ResponseBean login(String username, String password) {
//获取当前用户
Subject subject= SecurityUtils.getSubject();
//设置当前用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//登录认证 注意这里的帐号密码是跟Realm文件那边设置的进行对比验证
try{
subject.login(token);
} catch (IncorrectCredentialsException ice) {
// 捕获密码错误异常
return new ResponseBean(ResponseCode.Fail.GetValue(),"密码错误",null);
} catch (UnknownAccountException uae) {
// 捕获未知用户名异常
return new ResponseBean(ResponseCode.Fail.GetValue(),"账号异常",null);
} catch (ExcessiveAttemptsException eae) {
// 捕获错误登录过多的异常
return new ResponseBean(ResponseCode.Fail.GetValue(),"错误次数过多,账号被锁定,请于10分钟后再进行尝试",null);
}catch (LockedAccountException lae){
return new ResponseBean(ResponseCode.Fail.GetValue(),"帐号未激活",null);
}catch (AuthenticationException ae){
return new ResponseBean(ResponseCode.Fail.GetValue(),"帐号不存在",null);
}
ResponseBean responseBean = null;
Users user = queryUserByUserName(username);
subject.getSession().setAttribute("user", user);
SecurityUtils.getSubject().getSession().setTimeout(-1000l);
return new ResponseBean(ResponseCode.Success.GetValue(),"登陆成功",user);
}
}
然后在Realm文件中写方法
public class MyRealm extends AuthorizingRealm {
@Autowired
private UsersService usersService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权!!!!!");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证!!!!!");
//这里就获取登录时当前用户设置的登录信息
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//根据当前用户的用户名查找数据库的密码
Users users=usersService.queryUserByUserName(token.getUsername());
if(users!=null){
//根据用户名在数据库找不到,重返login页面
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("redirect:/login");
modelAndView.addObject("msg","账号不存在");
//return null;
throw new AuthenticationException("帐号不存在");
}
else{
//根据用户名在数据库找到用户了,那么获取密码,并返回,进行比较
return new SimpleAuthenticationInfo(users.getUsername(),users.getPassword(),this.getClass().getName());
}
}
}
至此,shiro的登录认证功能基本已配置完。
Realm文件登录验证测试(导入thymeleaf):
到实际测试阶段,这时,我选择偷懒,所以先导入themleaf依赖,还是pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
然后继续yml文件配置
spring下面加入
#thymeleaf设置
#模板编码
thymeleaf:
encoding: UTF-8
cache: false #启用模板缓存(开发时建议关闭)
mode: HTML5 #应用于模板的模板模式ervlet
servlet:
content-type: text/html #Content-Type值
enabled: true #启用MVC Thymeleaf视图分辨率
prefix: classpath:/templates/ #在构建URL时预先查看名称的前缀
suffix: .html #构建URL时附加查看名称的后缀
整体:
spring:
#热部署
devtools:
restart:
enabled: true
additional-paths: src/main/java
#数据库配置
datasource:
#1.JDBC
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xx.xxx.xxx.xxx/sardines?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
username: root
password: root
druid:
#2.连接池配置
#初始化连接池的连接数量 大小,最小,最大
initial-size: 5
min-idle: 5
max-active: 20
#配置获取连接等待超时的时间
max-wait: 60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 30000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: true
test-on-return: false
# 是否缓存preparedStatement,也就是PSCache 官方建议MySQL下建议关闭 个人建议如果想用SQL防火墙 建议打开
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filter:
stat:
merge-sql: true
slow-sql-millis: 5000
#3.基础监控配置
web-stat-filter:
enabled: true
url-pattern: /*
#设置不统计哪些URL
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
session-stat-enable: true
session-stat-max-count: 100
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: true
#设置监控页面的登录名和密码
login-username: admin
login-password: 123456
allow: 127.0.0.1
#deny: 122.1.1.1
#thymeleaf设置
#模板编码
thymeleaf:
encoding: UTF-8
cache: false #启用模板缓存(开发时建议关闭)
mode: HTML5 #应用于模板的模板模式ervlet
servlet:
content-type: text/html #Content-Type值
enabled: true #启用MVC Thymeleaf视图分辨率
prefix: classpath:/templates/ #在构建URL时预先查看名称的前缀
suffix: .html #构建URL时附加查看名称的后缀
#mybatis配置
mybatis:
#设置基本包 # 注意:对应实体类的路径
type-aliases-package: com.sardine.myproject.pojo
#告诉去哪找xml文件 #注意:一定要对应mapper映射xml文件的所在路径
mapper-locations: classpath:mybatis/mapper/*.xml
server:
port: 8080
至此,shiro框架的用户认证和thymeleaf配置就完成了。
实际使用
接下来,就是在前端页面引用thymeleaf,然后后端写代码进行操作了,我暂时做了个简单的登录,下面是具体代码。
ResponseCode文件和UserController文件:
public enum ResponseCode {
Success(200),
Fail(400),
Error(404);
private int value = 0;
private ResponseCode(int value) { //必须是private的,否则编译错误
this.value = value;
}
public int GetValue(){
return this.value;
}
}
@Controller
public class UserController {
@Autowired
private UsersMapper usersMapper;
@Autowired
private UsersService usersService;
@GetMapping({"/"})
public String toLogin(){
return "login";
}
@PostMapping("/login")
public String login(String username,String password){
System.out.println("username:"+username+" password:"+password);
ResponseBean responseBean= usersService.login(username,password);
if(responseBean.getCode()== ResponseCode.Success.GetValue()){
return "index";
}
else{
System.out.println("登录失败:"+responseBean.getMsg());
//System.out.println("登录失败");
return "login";
}
}
}
UserMapper.xml文件:
<?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="com.sardine.mapper.UsersMapper">
<resultMap id="userResultMap" type="com.sardine.pojo.Users">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="username" jdbcType="VARCHAR" property="username"/>
<result column="password" jdbcType="VARCHAR" property="password"/>
<result column="status" jdbcType="INTEGER" property="status"/>
</resultMap>
<sql id="BASE_TABLE">
users a
</sql>
<sql id="BASE_COLUMN">
a.*
</sql>
<select id="queryUserList" resultMap="userResultMap">
SELECT
<include refid="BASE_COLUMN"/>
FROM
<include refid="BASE_TABLE"/>
</select>
<select id="queryUserByUserName" resultMap="userResultMap" parameterType="String">
SELECT
<include refid="BASE_COLUMN"/>
FROM
<include refid="BASE_TABLE"/>
<where>
a.username = #{username}
</where>
</select>
<select id="queryUserById" resultMap="userResultMap" parameterType="int">
SELECT
<include refid="BASE_COLUMN"/>
FROM
<include refid="BASE_TABLE"/>
<where>
id = #{id}
</where>
</select>
</mapper>
前端文件:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body>
<h1>登录</h1>
<form method="post" th:action="@{/login}">
<p>用户名: <input type="text" name="username"></p>
<p>密码: <input type="text" name="password"></p>
<p><input type="submit"></p>
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录成功</h1>
</body>
</html>
PS:这里讲几个点:
1.themleaf要用<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">这句话必不可少
2.themleaf语法请自学
3.为了统一规格我额外建了个enum类,嫌麻烦直接写数字也可以
4.ReaponseBean是比较烂大街的自己写的,返回code、msg、data这三个数据的一个类,这里就不贴出来了。
5.由于我写的是加密的密码比较器,所以数据库那边的密码不是123456,而是md5加密后的数据
密码:123456 转换成这里写的md5加密后的结果是:e10adc3949ba59abbe56e057f20f883e
5.用户认证做完,继续完善Realm文件的权限认证
修改Realm文件,这里也就不写实际应用了,无非就是能进去,不能进去,然后在config文件已经设置过授权页面和未授权页面的跳转了。
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权!!!!!");
SimpleAuthorizationInfo Info = new SimpleAuthorizationInfo();
//获取登录后的用户,如果用户有admin角色或用户的Principal信息保存的是sardine用户名,那么添加权限
Subject subject= SecurityUtils.getSubject();
if(subject.hasRole("admin")||subject.getPrincipal().equals("sardine")) {
Info.addStringPermission("admin:play");
}
return Info;
}
PS:这里的addStringPermission里面的参数,可以改成通过自动装配注解,从数据库取权限名称。