本项目通过SpringBoot整合了shiro框架 使用MybatisPlus代码生成器生成简单的代码,项目包含shiro动态授权以及认证 ,rememberMe记住功能
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-shiro</name>
<description>Demo project for Spring Boot Shiro</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!--缓存-->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.4.8</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<!-- fastjson阿里巴巴jSON处理器 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 配置 fork 进行热部署支持 -->
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
server.port=80
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/
#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/crm?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#mybatis-plus
mybatis-plus.type-aliases-package=com.example.leilei.entity
#热部署
#设置开启热部署
spring.devtools.restart.enabled=true
#页面不加载缓存,修改即时生效
spring.freemarker.cache=false
@SpringBootApplication
@MapperScan(basePackages = "com.example.leilei.mapper")
public class SpringbootShiroApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootShiroApplication.class, args);
}
}
合理使用工具可以帮我减少开发时间
public class CodeGenerator {
public static void main(String[] args) throws InterruptedException {
//用来获取Mybatis-Plus.properties文件的配置信息
ResourceBundle rb = ResourceBundle.getBundle("springboot-shiro");
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir(rb.getString("OutputDir"));
gc.setFileOverride(true);
gc.setActiveRecord(true);// 开启 activeRecord 模式
gc.setEnableCache(false);// XML 二级缓存
gc.setBaseResultMap(true);// XML ResultMap
gc.setBaseColumnList(false);// XML columList
gc.setAuthor(rb.getString("author"));
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setDbType(DbType.MYSQL);
dsc.setTypeConvert(new MySqlTypeConvert());
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername(rb.getString("jdbc.user"));
dsc.setPassword(rb.getString("jdbc.pwd"));
dsc.setUrl(rb.getString("jdbc.url"));
mpg.setDataSource(dsc);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
// strategy.setTablePrefix(new String[] { "t_" });// 此处可以修改为您的表前缀
strategy.setNaming(NamingStrategy.underline_to_camel);// 表名生成策略
strategy.setInclude(new String[]{"real_eseate"}); // 需要生成的表
mpg.setStrategy(strategy);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent(rb.getString("parent"));
pc.setController("controller");
pc.setService("service");
pc.setServiceImpl("service.impl");
pc.setEntity("domain");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 注入自定义配置,可以在 VM 中使用 cfg.abc 【可无】
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
}
};
List<FileOutConfig> focList = new ArrayList<FileOutConfig>();
// 调整 xml 生成目录演示
focList.add(new FileOutConfig("/templates/mapper.xml.vm") {
@Override
public String outputFile(TableInfo tableInfo) {
return rb.getString("OutputDirXml")+ "/cn/leilei/mapper/" + tableInfo.getEntityName() + "Mapper.xml";
}
});
// 调整 query 生成目录
/* focList.add(new FileOutConfig("/templates/query.java.vm") {
@Override
public String outputFile(TableInfo tableInfo) {
return rb.getString("OutputDomainDir")+ "/cn/leilei/query/" + tableInfo.getEntityName() + "Query.java";
}
});*/
// 调整 domain 生成目录
focList.add(new FileOutConfig("/templates/entity.java.vm") {
@Override
public String outputFile(TableInfo tableInfo) {
return rb.getString("OutputDomainDir")+ "/cn/leilei/domain/" + tableInfo.getEntityName() + ".java";
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 自定义模板配置,可以 copy 源码 mybatis-plus/src/main/resources/templates 下面内容修改,
// 放置自己项目的 src/main/resources/templates 目录下, 默认名称一下可以不配置,也可以自定义模板名称
TemplateConfig tc = new TemplateConfig();
//tc.setController("/templates/controller.java.vm");
// 如上任何一个模块如果设置 空 OR Null 将不生成该模块。
tc.setEntity(null);
tc.setXml(null);
mpg.setTemplate(tc);
// 执行生成
mpg.execute();
}
}
springboot-shiro.perproties
#输出路径
OutputDir=E://EEworkspac//springbootshiro//springboot-shiro//src//main//java
#query和domain的生成路径
OutputDomainDir=E://EEworkspac//springbootshiro//springboot-shiro//src//main//java
#作者
author=lei
#数据源
jdbc.user=root
jdbc.pwd=root
jdbc.url=jdbc:mysql:///crm?useUnicode=true&characterEncoding=utf8
#包配置
parent=cn.leilei
#xml的生成的resources目录
OutputDirXml=E://EEworkspac//springbootshiro//springboot-shiro//src//main//resources
这个shiro生命周期的Bean一定要单独配置
/**
* 这个shiro生命周期的Bean一定要单独配置
*/
@Configuration
public class ShiroLifecycleBeanPostProcessorConfig {
/**
* Shiro生命周期处理器
*
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
public class MyRealm extends AuthorizingRealm {
@Autowired
private IEmployeeService employeeService;
@Autowired
private IPermissionService permissionService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
Employee employee = (Employee) principalCollection.getPrimaryPrincipal();
Long empId = employee.getId();
//根据员工id查询员工权限
List<Permission> permissions = permissionService.getByEmpId(empId);
//将查询出的权限交给shiro
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (Permission permission : permissions) {
info.addStringPermission(permission.getSn());
}
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
//根据用户名查询用户
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Employee employee = employeeService.getOne(wrapper);
if(employee==null){
throw new UnknownAccountException(username);
}
//封装info对象
return new SimpleAuthenticationInfo(employee,employee.getPassword(), ByteSource.Util.bytes(MD5Utils.SALT),getName());
}
}
授权方法需要连表查询
根据elmplyee 查询到permission表
简单的写了下,但在实际开发中sql语句不要使用 *
<select id="selectByEmpId" parameterType="long" resultType="com.example.leilei.entity.Permission">
SELECT p.*
from t_employee_role er
left join t_role r on er.role_id = r.id
left join t_role_permission rp on r.id = rp.role_id
left join t_permission p on rp.permission_id = p.id
where er.employee_id = #{empId}
</select>
目前项目中包含认证,授权,rememberMe
package com.example.leilei.config;
import com.example.leilei.entity.Permission;
import com.example.leilei.service.IEmployeeService;
import com.example.leilei.service.IPermissionService;
import com.example.leilei.shiro.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Configuration
@AutoConfigureAfter(ShiroLifecycleBeanPostProcessorConfig.class)//配置Bean加载的先后顺序
public class ShiroConfig {
@Autowired
private IPermissionService permissionService;
/**
* SecurityManager核心对象Bean
* @return
*/
@Bean(name = "securityManager")
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//注入Realm
securityManager.setRealm(myRealm());
//注入记住我管理器
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* 凭证比较器-加密加盐加次数
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//加密算法
hashedCredentialsMatcher.setHashIterations(10);//加密次数
return hashedCredentialsMatcher;
}
/**
* cookie对象;
* rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
* @return
*/
@Bean
public SimpleCookie rememberMeCookie(){
//System.out.println("ShiroConfiguration.rememberMeCookie()");
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//<!-- 记住我cookie生效时间30天 ,单位秒;-->
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
/**
* cookie管理对象;
* rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager(){
//System.out.println("ShiroConfiguration.rememberMeManager()");
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
return cookieRememberMeManager;
}
/**
* 自定义的Realm
* @return
*/
@Bean
public MyRealm myRealm(){
MyRealm myRealm = new MyRealm();
//设置密码加密凭证,登录时会对密码进行加密匹配
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myRealm;
}
/**
*过滤器配置 过滤所有权限
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//配置过滤器
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
filterChainDefinitionMap.put("/static/**", "anon"); //anon代表资源直接放行
filterChainDefinitionMap.put("/logout", "logout"); //shiro的退出方法,会注销自己的认证
//权限拦截,查出所有权限
List<Permission> permissions = permissionService.list();
for (Permission permission : permissions) {
filterChainDefinitionMap.put(permission.getUrl(),"perms["+permission.getSn()+"]");
}
filterChainDefinitionMap.put("/**", "authc");
//当访问需要认证才能访问的资源,如果没有认证,则跳转到这个资源
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//当访问需要授权才能访问的资源的时候,如果没有权限,则跳转到这个资源
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
/**
* 这个类用来返回ajax对象,并且私有化字段,只能通过构造方法赋值
*
*/
public class AjaxResult {
private Boolean success = true;
private String msg;
private AjaxResult() {
}
/**
* 返回ajax
* @return 没有错误就返回这个
*/
public static AjaxResult success(){
return new AjaxResult();
}
/**
* 返回ajax
* @param msg 错误信息
* @return 有错误就返回这个
*/
public static AjaxResult error(String msg){
AjaxResult ajaxResult = success();;
ajaxResult.setSuccess(false);
ajaxResult.setMsg(msg);
return ajaxResult;
}
public Boolean getSuccess() {
return success;
}
private void setSuccess(Boolean success) {
this.success = success;
}
public String getMsg() {
return msg;
}
private void setMsg(String msg) {
this.msg = msg;
}
}
public class SessionUtil {
public static final String LOGINSESSION = "loginuser";
//将登陆对象存入域对象之中
public static void setSession(Employee employee){//将登陆用户存入域对象
Subject subject = SecurityUtils.getSubject();//获取登陆对象
subject.getSession().setAttribute(LOGINSESSION,employee);
}
//Session中获取当前登陆对象
public static Employee getSession(){//获取登陆对象域对象信息
Subject subject = SecurityUtils.getSubject();//获取登陆对象
return (Employee) subject.getSession().getAttribute(LOGINSESSION);
}
}
public class MD5Utils {
public static final String SALT = "fm";
public static final int ITERATIONS = 10;
/**
* 加密
* @param source
* @return
*/
public static String encrype(String source){
SimpleHash simpleHash = new SimpleHash("MD5",source,SALT,ITERATIONS);
return simpleHash.toString();
}
public static void main(String[] args) {
System.out.println(encrype("admin"));
}
}
@Controller
public class LoginController {
/**
* 跳转到登录页面
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String login(){
Subject subject = SecurityUtils.getSubject();
//判断当前用户是否有使用rememberMe
if (subject.isRemembered()){
return "main";
//判断当前用户是否登录
}else if(subject.isAuthenticated()){
return "main";
}
return "login";
}
/**
* 登录请求
* @param employee
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
@ResponseBody
public AjaxResult login(@RequestBody Employee employee){
//尝试获取获取用户信息
Subject subject = SecurityUtils.getSubject();
/*Boolean rememberMe = true;*/
//将输入的账户密码封装为一个tocken对象
UsernamePasswordToken token = new UsernamePasswordToken(employee.getUsername(),employee.getPassword(),employee.getRememberMe());
try {
//使用封装的tocken对象尝试通过shiro完成认证---->会调用自定义MyRealm类的AuthenticationInfo方法,尝试认证,
if(!subject.isAuthenticated()){//判断当前用户是否登录 布尔值 取反
subject.login(token);//认证登陆
}
//将登陆的用户信息存入域对象之中
Employee logiuser = ((Employee) subject.getPrincipal());//获取当前用户
SessionUtil.setSession(logiuser);
return AjaxResult.success();
} catch (UnknownAccountException e) {
e.printStackTrace();
return AjaxResult.error("账户不存在");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
return AjaxResult.error("密码错误");
} catch (AuthenticationException e) {
e.printStackTrace();
return AjaxResult.error("未知错误,检查后台");
}
}
//退出登录
@RequestMapping(value = "/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
return "redirect:/logout";
}
}
前端所用的静态资源放在 resource/statc下
前端所用的页面放在 resource/templates下
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<base th:href="${#request.getContextPath()}+'/'">
<meta charset="UTF-8">
<title>Title</title>
<script src="static/plugins/vuejs/vue.min.js" type="text/javascript"></script>
<link rel="stylesheet" href="static/plugins/elementui/lib/theme-chalk/index.css">
<script src="static/plugins/elementui/lib/index.js"></script>
<script src="static/plugins/vuejs/axios.js" type="text/javascript"></script>
<style type="text/css">
.login-container {
/*box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.06), 0 1px 0px 0 rgba(0, 0, 0, 0.02);*/
-webkit-border-radius: 5px;
border-radius: 5px;
-moz-border-radius: 5px;
background-clip: padding-box;
margin: 180px auto;
width: 350px;
padding: 35px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
.rememberMe {
margin: 0px 0px 35px 0px;
}
</style>
</head>
<body>
<div id="app">
<el-form :model="loginUser" :rules="loginFormRules" ref="loginForm" label-position="left" label-width="0px"
class="demo-ruleForm login-container">
<h3 class="title">员工登录</h3>
<el-form-item prop="username">
<el-input type="text" v-model="loginUser.username" auto-complete="off" placeholder="账号"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" v-model="loginUser.password" auto-complete="off" placeholder="密码"
@keyup.native.enter="handleLogin"></el-input>
</el-form-item>
<el-checkbox v-model="checked" class="rememberMe" name="rememberMe">记住密码</el-checkbox>
<el-form-item style="width:100%;">
<el-button type="primary" style="width:100%;" @click.native.prevent="handleLogin" :loading="logining">登录
</el-button>
</el-form-item>
</el-form>
</div>
<script>
new Vue({
el: "#app",
data: {
logining: false,
loginUser: {
username: '',
password: '',
rememberMe:false
},
loginFormRules: {
username: [
{required: true, message: '请输入账号', trigger: 'blur'},
],
password: [
{required: true, message: '请输入密码', trigger: 'blur'},
]
},
checked: false,
},
methods: {
handleLogin(ev) {
var _this = this;
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.logining = true;
//发送登录请求
this.loginUser.rememberMe=this.checked
axios.post("login", this.loginUser)
.then((res) => {
let data = res.data;
if (data.success) {
//登录成功
this.logining = false;
//跳转到首页
location.href = "main";
} else {
this.logining = false;
this.$message.error(data.msg, "error");
}
})
} else {
this.$message.error('请完成用户名密码的输入');
return false;
}
});
}
}
});
</script>
</body>
</html>
其他页面按着编写就好 最终采用我的源码会完成动态授权以及认证以及rememberMe的功能实现