Shiro?
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序
Shiro中有三大对象:Realm、Subject、SecuritytManager
绝大部分对于项目的说明写在代码注释中
此博客中的项目基于SpringBoot(2.6.7)整合Mybatis项目创建,其中大部分依赖版本依据SpringBoot(2.6.7)而定,小部分官方未提供版本建议需自行指定
一.导入依赖
<dependencies>
<!--thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--thymeleaf与shiro整合包-->
<!--可用thymeleaf进行验证(需要命名空间xmlns:sec="http://www.thymeleaf.org/extras/spring-security")-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--shiro依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.9.0</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--mysql connector-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--log4j-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!--devtools-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>2.6.7</version>
</dependency>
</dependencies>
二.配置yml文件
server:
port:
8080
mybatis:
mapper-locations: classpath:mappers/*.xml
type-aliases-package: cn.alan.springbootshiro.POJO
spring:
#选择启用的环境
profiles:
active: dev
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456789
#德鲁伊数据源
type: com.alibaba.druid.pool.DruidDataSource
druid:
#SpringBoot默认是不注入 需要自己绑定至bean(使用java配置bean时 因为springboot内置了servlet容器 所以无web.xml 需要@Bean将配置注入)
#druid数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters。stat:监控统计、wall:防御sql注入、log4j:日志记录
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
#配置 DruidStatFilter
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: /druid/*,*.js,*.css,*.gif,*.jpg,*.bmp,*.png,*.ico
#配置 DruidStatViewServlet
stat-view-servlet:
#访问德鲁伊监控页面的地址
url-pattern: /druid/*
#IP白名单 没有配置或者为空 则允许所有访问
allow: 127.0.0.1
#IP黑名单 若白名单也存在 则优先使用
# deny: ip地址
#禁用重置按钮
# reset-enable: false
#登录德鲁伊监控页面所用的用户名与密码
login-username: root
login-password: 123456
#关闭thymeleaf缓存 修改代码后无需重启即可更新
thymeleaf:
cache: false
三.代码部分
DAO层(注意@Repository与@Mapper注解)
@Mapper
@Repository
public interface UserMapper {
User queryUserByName(String username);
}
Service层(注意@Service注解)
Service实现类需要实现UserDetails接口
public interface UserService {
User queryUserByName(String username);
}
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserMapper userMapper;
@Override
public User queryUserByName(String username) {
return userMapper.queryUserByName(username);
}
}
Controller层(注意@Controller注解)
package cn.alan.springbootshiro.Controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class RoutingController {
@RequestMapping({"/", "/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello shiro");
return "index";
}
@RequestMapping("/user/add")
public String toAdd(Model model){
return "/user/add";
}
@RequestMapping("/user/update")
public String toUpdate(Model model){
return "/user/update";
}
//登陆页面
@RequestMapping("/toLogin")
public String toLogin(Model model){
return "login";
}
//登录处理(处理login.html中表单提交的数据)
@RequestMapping("/login")
public String login(String username,String password,Model model){
//通过Subject获取当前用户
Subject subject = SecurityUtils.getSubject();
//将当前用户的登录数据封装为token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
//将token传入subject的login方法 若无异常则完成登录
try {
subject.login(usernamePasswordToken);
//无异常 返回首页
return "index";
} catch (AuthenticationException e) {
//存在异常 停留在登陆页面 并输出提示
model.addAttribute("msg","wrong username or password");
return "login";
}
}
//注销 logout
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "index";
}
//未授权页面
@RequestMapping("/unAuthorized")
public String unAuthorized(){
return "unAuthorized";
}
}
POJO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
int id;
String username;
String password;
String perms;
String role;
}
Config
Realm类需要继承AuthorizingRealm类
//自定义realm对象 继承AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权Authorization");
//授权信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//获取当前进行登录的对象
Subject subject = SecurityUtils.getSubject();
//获取User对象
User currentUser = (User) subject.getPrincipal();
//设置当前用户权限
simpleAuthorizationInfo.addStringPermission(currentUser.getPerms());
simpleAuthorizationInfo.addRole(currentUser.getRole());
return simpleAuthorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证Authentication");
//将token转换为UsernamePasswordToken
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
//从数据库取值
User user = userService.queryUserByName(usernamePasswordToken.getUsername());
if (user==null){
throw new AuthenticationException("unknown username");//抛出异常
}
//密码认证 由shiro完成
//可以进行加密 默认为Simple加密(SimpleCredentialsMatcher)
//第一个参数为principal 可在Subject对象中通过getPrincipal()接受
return new SimpleAuthenticationInfo(user,user.getPassword(),"");
}
}
package cn.alan.springbootshiro.Config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
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.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//创建Realm对象 需要自定义
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
//DefaultWebSecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//SecurityManager关联realm
defaultWebSecurityManager.setRealm(userRealm());
return defaultWebSecurityManager;
}
//ShiroFilterFactoryBean 过滤器(授权)
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//Filter关联SecurityManager
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager());
//shiro内置过滤器
/*
anon: 无需认证即可访问
authc: 必须认证了才能访问
user: 必须拥有记住我功能才能访问
perms: 拥有对某个资源的访问权限才能访问
role: 拥有某个角色权限才能访问
*/
//收集页面授权信息
Map<String,String> filterMap = new LinkedHashMap<>();
//权限验证 若无权限则跳转至未授权页面
//filterMap.put("/user/add","roles[user01]");//必须拥有user角色才能访问
//filterMap.put("/user/update","perms[add,update]");//必须同时拥有add与update权限才能访问
filterMap.put("/user/add","perms[add]");//必须拥有add权限才能访问
filterMap.put("/user/update","perms[update]");//必须拥有update权限才能访问
//注:数据库字段若为通配符 则代表拥有所有角色或权限
//设置页面授权
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//设置登录页面
shiroFilterFactoryBean.setLoginUrl("/toLogin");
//设置未授权跳转页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unAuthorized");
return shiroFilterFactoryBean;
}
//整合ShiroDialect 用于整合shiro与thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
}
数据库
资源目录结构
index.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
>
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<h1>index</h1>
<!--从controller中取值(处理index.html页面的mapping)-->
<p th:text="${msg}"></p>
<hr>
<div shiro:guest="true"><!--游客访问时显示-->
<a th:href="@{/toLogin}">Login</a>
</div>
<div shiro:authenticated="true"><!--登录后显示-->
<a th:href="@{/logout}">Logout</a>
</div>
<div shiro:hasPermission="add"><!--拥有add权限时显示-->
<a th:href="@{/user/add}">Add</a>
</div>
<div shiro:hasPermission="update"><!--拥有update权限时显示-->
<a th:href="@{/user/update}">update</a>
</div>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Login</h1>
<!--从controller中取值(处理login.html的mapping处)-->
<p th:text="${msg}" style="color: red"></p>
<form th:action="@{/login}">
<input type="text" name="username"><strong>Username</strong>
<br>
<br>
<input type="text" name="password"><strong>Passwrod</strong>
<p><input type="submit" value="Submit"></p>
</form>
</body>
</html>
add.html(update.html、unAuthorized.html与此大同小异,不赘述)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>add</h1>
</body>
</html>
运行效果
访问localhost:8080进入首页,点击登录按钮进入登录页面
登录页面
登录失败提示
不同的账户拥有的权限、角色不同,看到的界面亦不同
注销后返回首页