最近抽空研究了目前比较常用的轻量级安全框架Apache shiro,首先查看了很多现有的博客,打好理论基础,然后学以致用,码代码出学习成果,收获颇丰!
推荐本人参考过的优秀博客
####参考博客
SpringBoot+shiro整合学习之登录认证和权限控制
https://www.cnblogs.com/ll409546297/p/7815409.html
https://www.cnblogs.com/expiator/p/8651798.html
####参考博客
当然每个人的兴趣点不同,所以对技术性的博客有自己的选择,不过我初入门一门新技术时,都是从理论先入手,搞清楚“是什么?能干什么?”,然后再实践加深理解。本篇博客主要介绍springboot+thymeleaf+apache shiro实践应用,关于shiro的理论基础,小伙伴们自行学习了。
shiro应用主要包含db table、realm、shiroConfig、shiroController、html登录等几部分的创建和编码。
1.首先创建数据表,之前要理清登录用户、角色和权限的关系。一个应用有多个用户,每个用户有多个角色(管理员、游客等),每个角色具有多个权限(查看、添加、删除等)。我这里可提供的建表参考如下,小伙伴可根据自己的情况添加一些数据以备后面测试用:
CREATE TABLE `u_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`nickname` varchar(20) DEFAULT NULL COMMENT '用户昵称',
`email` varchar(128) DEFAULT NULL COMMENT '邮箱|登录帐号',
`pswd` varchar(32) DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
`status` bigint(1) DEFAULT '1' COMMENT '1:有效,0:禁止登录',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8;
CREATE TABLE `u_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL COMMENT '角色名称',
`type` varchar(10) DEFAULT NULL COMMENT '角色类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
CREATE TABLE `u_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`url` varchar(256) DEFAULT NULL COMMENT 'url地址',
`name` varchar(64) DEFAULT NULL COMMENT 'url描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
CREATE TABLE `u_user_role` (
`uid` bigint(20) DEFAULT NULL COMMENT '用户ID',
`rid` bigint(20) DEFAULT NULL COMMENT '角色ID'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `u_role_permission` (
`rid` bigint(20) DEFAULT NULL COMMENT '角色ID',
`pid` bigint(20) DEFAULT NULL COMMENT '权限ID'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.在pom.xml中添加Apache shiro依赖
<!--apache shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
3.realm--认证、鉴权逻辑编码,另外重写了凭据比对器
public class MyShiroRealm extends AuthorizingRealm {
private static Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
@Autowired
private UUserService uUserService;
/**
* 第一步:登录认证,用户名、密码和数据库比对
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
List<UUserVo> userVoList = uUserService.findUserByUsername(username);//查询数据库
if(userVoList==null || userVoList.size()==0){
throw new AccountException("帐号或密码不正确!");
}
UUserVo user = userVoList.get(0);
if(user.getStatus()==0){
throw new DisabledAccountException("该账号禁止登录!");
}
return new SimpleAuthenticationInfo(user, user.getPswd(), this.getName());
}
/**
* 第二步:角色、权限认证
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
UUserVo user = (UUserVo) principalCollection.getPrimaryPrincipal();
Set<String> permissions = new HashSet<>();//权限去重
Set<String> roles = new HashSet<>();//角色去重
List<UUserVo> userVoList = uUserService.findUserByUsername(user.getEmail());
for (UUserVo userVo : userVoList){
roles.add(userVo.getRole());
permissions.add(userVo.getPermission());
}
logger.info("logback: 角色="+roles);
logger.info("logback: 权限="+permissions);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(permissions);
return simpleAuthorizationInfo;
}
}
其中,主要的dao用户对象查询逻辑如下:
public interface UUserMapper extends Mapper<UUser> {
@Select("SELECT u.id, u.nickname, u.email, u.pswd, u.`status`, r.`name` as role, p.`name` as permission" +
" FROM u_user u inner join u_user_role ur on ur.uid=u.id" +
" inner join u_role r on r.id=ur.rid" +
" inner join u_role_permission rp on rp.rid=r.id" +
" inner join u_permission p on p.id=rp.pid" +
" WHERE u.email=#{username}")
List<UUserVo> findUserByUsername(@Param("username") String username);
}
其中,重写了凭据比对器
/**
* 凭据比对器
* 重写doCredentialsMatch()
*/
public class CredentialMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String inputPswd = new String(((UsernamePasswordToken)token).getPassword());
String dbPswd = (String) info.getCredentials();
return this.equals(inputPswd, dbPswd);
}
}
4.shiro config类至关重要,其中最为重要的是过滤器配置,详细代码如下:
@Configuration
public class ShiroConfig {
private static Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
/**
* 认证(用户名、密码、权限)逻辑
* @return
*/
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm realm = new MyShiroRealm();
return realm;
}
/**
* 安全管理器,shiro的核心组件,管理所有subject
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* 处理资源拦截
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/shiro/goLogin");//登录认证页面
shiroFilterFactoryBean.setSuccessUrl("/shiro/goSuccess");//认证成功跳转页面
shiroFilterFactoryBean.setUnauthorizedUrl("/shiro/goError");//未授权页面
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不会被拦截的静态资源路径
filterChainDefinitionMap.put("/static/**", "anon");
//ajax登录url不被拦截
filterChainDefinitionMap.put("/shiro/login", "anon");
//退出登录
filterChainDefinitionMap.put("/shiro/logout", "logout");
filterChainDefinitionMap.put("/shiro/goAdd", "perms[add]");//权限名称需一致
//对所有用户认证
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
logger.info("logback: shiro拦截器工厂类注入完成!");
return shiroFilterFactoryBean;
}
}
5.shiro controller控制器类编码,其中最主要的是登录逻辑、退出逻辑,其他的都是页面跳转
@Controller
@RequestMapping("/shiro")
public class ShiroController {
/**
* ajax form login
* @param username
* @param password
* @return
* @throws Exception
*/
@RequestMapping("/login")
@ResponseBody
public ResultResponse login(@RequestParam(value = "username", required = true) String username,
@RequestParam(value = "password", required = true) String password) throws Exception{
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
subject.login(token);//登录认证、鉴权
return ResultResponse.ok("login success");
}
@RequestMapping("/goLogin")
public String goLogin(){
return "login";
}
@RequestMapping("/goSuccess")
public String goSuccess(){
return "success";
}
@RequestMapping("/goError")
public String goError(){
return "error";
}
//添加权限页面
@RequestMapping("/goAdd")
public String goAdd(){
return "add";
}
@RequestMapping("/logout")
@ResponseBody
public Object logout(){
try {
//退出 跳转到登录页面
SecurityUtils.getSubject().logout();
} catch (Exception e) {
System.err.println(e.getMessage());
}
return "退出登录";
}
}
6.应用的resources目录如下图,涉及到静态资源(js、css)在static目录下,html页面在templates目录下
7. 贴下login.html页面的代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>登录</title>
<!-- boostrap css -->
<link rel="stylesheet" th:href="@{/static/boostrap/css/bootstrap.min.css}">
<!-- sweetalert css -->
<link rel="stylesheet" th:href="@{/static/sweetAlert/dist/sweetalert2.min.css}" />
<!-- jquery -->
<script th:src="@{/static/js/jquery-3.4.0.min.js}" src=""></script>
<!-- bootstrap js -->
<script th:src="@{/static/boostrap/js/bootstrap.min.js}"></script>
<!-- sweetalert js -->
<script th:src="@{/static/sweetAlert/dist/sweetalert2.min.js}"></script>
<!-- md5 js -->
<script th:src="@{/static/js/md5.min.js}"></script>
<script th:src="@{/static/js/common.js}"></script>
<style>
.center {
margin: auto;
width: 50%;
margin-top: 200px;
}
</style>
</head>
<body>
<div class="container">
<form class="form-horizontal center" role="form">
<div class="form-group">
<label for="mobile" class="col-md-2 control-label">账号</label>
<div class="col-md-10">
<input type="text" class="form-control" id="mobile" placeholder="请输入账号">
</div>
</div>
<div class="form-group">
<label for="password" class="col-md-2 control-label">密码</label>
<div class="col-md-10">
<input type="password" class="form-control" id="password" placeholder="请输入密码">
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button type="button" class="btn btn-success btn-block" id="to_login">登录</button>
</div>
</div>
</form>
</div>
<script>
$(function() {
$("#to_login").click(function() {
var account = $("#mobile").val();
var inputPass = $("#password").val();
if (app.notEmpty(account) && app.notEmpty(inputPass)) {
//var salt = app.formSalt;
//var str = "" + salt.charAt(0) + salt.charAt(1) + inputPass + salt.charAt(3) + salt.charAt(4);
//var password = md5(str);
// 登录认证
$.ajax({
url: "/shiro/login",
type: "POST",
dataType:"json",
data: {
username: account,
password: inputPass
},
success: function(data) {
if(data.status==200){
//alert(data.data);
//app.littleTip(data.data);
location.href="/shiro/goSuccess";
}else{
//app.littleTip(data.msg);
location.href="/shiro/goError";
}
},
error: function(xhr,errorType) {
app.littleTip("fail:"+xhr.readyState+","+xhr.status);
}
});
} else {
app.littleTip("账号或密码不能为空")
}
});
})
</script>
</body>
</html>
8.实践过程中的感悟和大家分享
(1)ShiroConfig类中配置过滤器要根据自己的实际需要,本文中配置的较为简单,感觉这部分对于初学者来说稍有难度,不过多测试思考即可发现问题,理解会加深!
(2)thymeleaf下html访问static静态资源方法需注意。最初没有加上“/static/”前缀是行不通的。