1.项目的整体结构
项目整体采用分模块开发,分别有:
–basic-core模块:主要用于存放一些父类,比如BaseService、BaseMapper等,该模块依赖basic-utils模块
–basic-utils模块:主要用于存放各个公共类的抽取
–rpms-common模块:主要是项目三层公共部分的抽取,比如domain、query等,该模块依赖basic-core模块
–rpms-mapper模块:该模块是项目的持久层,类似dao层,包含mapper接口、映射,与一些持久层的配置,依赖rpms-common、basic-core模块
–rpsm-service模块:该模块是业务层,依赖rpms-mapper模块
–rpms-web模块:该模块是项目的web层,依赖rpsm-service模块
对于依赖关系的配置,在每个模块的pom.xml中配置依赖,例如rpms-web配置与service的依赖
<dependency>
<groupId>cn.itsource</groupId>
<artifactId>rpms-service</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2.登录模块的分析
1.实现登录功能,需要对登录用户的账号密码进行判断,如果与数据库一致才能通过登录验证;
2.登录用户的密码应该进行加密处理,提高安全性;
3.对于不同的用户群体,登录进主页面所展示的效果应该有所不同;
3.登录用户的信息判断,引入shiro框架
1.pom.xml配置
<!--shiro支持包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.4.0</version>
<type>pom</type>
</dependency>
<!-- shiro与Spring的集成包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2.在rpsm-service层,创建applicationContext-shiro.xml配置文件,并且让applicationContext-service.xml对其进行引用
–配置文件中主要有自定义的realm的配置,自定义拦截器的配置以及用户密码加密的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!-- securityManager:Spring创建核心对象 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="rpmsRealm"/>
</bean>
<bean id="rpmsRealm" class="cn.itsource.rpms.shiro.RpmsRealm">
<property name="credentialsMatcher">
<bean class="cn.itsource.rpms.shiro.CybHashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="10" />
</bean>
</property>
</bean>
<!-- 建议大家把它留着,它可以支持我们做注解权限判断 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!--
Shiro中真正的过滤器:名称必需和web.xml中的那个过滤器名字一样
-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!--如果你没有登录,就会进入这个页面-->
<property name="loginUrl" value="/login"/>
<!--如果登录成功,就会进入这个页面-->
<property name="successUrl" value="/main"/>
<!--如果没有权限,就会进入这个页面-->
<property name="unauthorizedUrl" value="/bad.jsp"/>
<!--注入权限-->
<property name="filterChainDefinitionMap" ref="filterChainDefinitionBean" />
<!--配置自定义拦截器-->
<property name="filters">
<map>
<entry key="npms" value-ref="npmsPermissionsAuthorizationFilter"></entry>
</map>
</property>
</bean>
<!--配置自定义拦截器-->
<bean id="npmsPermissionsAuthorizationFilter" class="cn.itsource.rpms.shiro.RpmsPermissionsAuthorizationFilter"/>
<!--配置获取登陆权限map集合的方法的bean-->
<bean id="filterChainDefinitionBean" factory-bean="filterChainDefinitionMapBean" factory-method="creatPermisissionMap"></bean>
<!--配置获取登陆权限map集合的bean-->
<bean id="filterChainDefinitionMapBean" class="cn.itsource.rpms.shiro.CreatPermisissionMap"></bean>
</beans>
3.创建自定义的realm类,用于对比用户登录的信息和数据库中的信息,自定义的realm需要在shiro.xml的配置文件中引用
/**
* 自定义realm
*/
public class RpmsRealm extends AuthorizingRealm {
@Autowired
private IUserService userService;
@Autowired
private IPermissionService permissionService;
//权限控制
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//得到权限验证对象
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//拿到当前主题
User user = UserContext.getUser();
//传入id拿到数据库对应的权限
Set<String> permissionSnByCurrentUserId = permissionService.findPermissionSnByCurrentUserId(user.getId());
//
simpleAuthorizationInfo.setStringPermissions(permissionSnByCurrentUserId);
return simpleAuthorizationInfo;
}
//登录管理
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.拿到令牌
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//2.拿到用户名
String username = token.getUsername();
//3.根据用户名拿出相应的用户
User user = userService.findByName(username);
if(user==null){
//代表登录失败 ,返回空会自动告诉咱们用户名错误
return null;
}
//准备盐值
ByteSource salt = ByteSource.Util.bytes("itsource");
/**
* principal:主体
*/
SimpleAuthenticationInfo authenticationInfo =
new SimpleAuthenticationInfo(user,user.getPassword(),salt,"sil");
return authenticationInfo;
}
}
4.创建自定义的拦截器,主要用于对一些页面的访问权限拦截处理,这种方式比直接在shiro中配置拦截处理更加的灵活。自定义拦截器也需要在shiro.xml中进行配置
public class CreatPermisissionMap {
@Autowired
private IPermissionService permissionService;
@Autowired
private IMenuService menuService;
public Map<String ,Object> creatPermisissionMap(){
Map<String,Object> map=new LinkedHashMap<>();
map.put("/index.jsp","anon");
map.put("/login/index","anon");
map.put("/login","anon");
map.put("/check.jpg","anon");
map.put("/user/checkName","anon");
map.put("/login/regist", "anon");
map.put("/static/login/*", "anon");
map.put("/register.jsp", "anon");
map.put("/login/getcode", "anon");
map.put("/login/save", "anon");
map.put("/login/getcode", "anon");
map.put("/user/reg", "anon");
map.put("/user/regWechat", "anon");
map.put("/reg", "anon");
map.put("/regOld", "anon");
map.put("/static/images/*", "anon");
map.put("/static/css/*", "anon");
map.put("/login.html", "anon");
map.put("/wechat/*", "anon");
map.put("/static/js/**","anon");
map.put("/static/**","anon");
map.put("*.js","anon");
map.put("*.css","anon");
map.put("/css/**","anon");
map.put("/static/js/plugins/**","anon");
map.put("/static/login/js/*","anon");
map.put("/static/login/css/*","anon");
map.put("static/login/webfonts/*","anon");
map.put("/images/**","anon");
map.put("/logout", "logout");
List<Permission> permissions = permissionService.findAll();
for(Permission permission:permissions){
map.put(permission.getUrl(),"npms["+permission.getSn()+"]");
}
map.put("/**","authc");
return map;
}
}
5.创建一个自定义的加密工具MD5Utils,注意这个工具中的加密必须与shiro.xml中加密的方式保持一致
/**
* 我们系统自己设置的加密规则
*/
public class MD5Util {
public static final String SALT = "itsource";
//Ctrl+Shift+y 全变大写
public static final int HASHITERATIONS = 10;
public static String createMd5Str(String pwd){
SimpleHash hash = new SimpleHash("MD5",pwd,SALT,HASHITERATIONS);
return hash.toString();
}
}
6.创建一个工具类UserContext,这个类主要用于从session中获取登录用户,以及将登录用户存入session中
public class UserContext {
public static final String USER_IN_SESSION = "userInSession";
//设置用户到session中
public static void setUser(User loginUser){
Subject subject = SecurityUtils.getSubject();
subject.getSession().setAttribute(USER_IN_SESSION,loginUser);
}
//为Session中获取到当前登录用户
public static User getUser(){
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getSession().getAttribute(USER_IN_SESSION);
return user;
}
}
7.登录页面前端的处理,前端通过form表单将数据提交到后台,这里使用的前端框架是layui
–html部分:
<div class="login">
<h1>RPMS后台登录</h1>
<form class="layui-form">
<div class="layui-form-item">
<input class="layui-input" name="name" placeholder="用户名" value="admin1" lay-verify="required" type="text" autocomplete="off">
</div>
<div class="layui-form-item">
<input class="layui-input" name="password" placeholder="密码" value="123456" lay-verify="required" type="password" autocomplete="off">
</div>
<div class="layui-form-item form_code">
<input class="layui-input" name="codeImg" placeholder="验证码" lay-verify="required" type="text" autocomplete="off">
<div class="code"><img id="imgs" src="/check.jpg" width="116" height="36"></div>
</div>
<hr>
<a id="reg" href="javascript:void(0);" class="fl" style="color: #dce2ec">立即注册</a><a id="reset" href="/wechat/login" class="fr" style="color: #dce2ec">第三方登录</a>
<button type="button" id="log" class="layui-btn login_btn" lay-submit="" lay-filter="login">登录</button>
</form>
</div>
–js部分
// 登录方法
form.on('submit(login)', function (data) {
$.post("/login", data.field, function (result) {
if (result.success) {
window.location.href = "/main/index";
} else {
layer.alert(result.msg, {
icon: 2,
title: "提示"
});
}
})
})
//刷新验证码
$("#imgs").on("click",function () {
var url = "/check.jpg?number="+Math.random();
$("#imgs").attr("src",url);
})
//回车登录功能
$(document.documentElement).on("keyup", function(event) {
var keyCode = event.keyCode;
if (keyCode === 13) { // 捕获回车
$("#log").trigger("click");
}
});
8.后台controller层接收数据,并进行数据处理
–这里集成了一个验证码生成器,工具类CaptchaUtil
public class CaptchaUtil {
private BufferedImage image;// 图像
private String str;// 验证码
private static char code[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789".toCharArray();
public static final String SESSION_CODE_NAME="code";
private CaptchaUtil() {
init();// 初始化属性
}
/*
* 取得RandomNumUtil实例
*/
public static CaptchaUtil Instance() {
return new CaptchaUtil();
}
/*
* 取得验证码图片
*/
public BufferedImage getImage() {
return this.image;
}
/*
* 取得图片的验证码
*/
public String getString() {
return this.str;
}
private void init() {
// 在内存中创建图象
int width = 85, height = 20;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取图形上下文
Graphics g = image.getGraphics();
// 生成随机类
Random random = new Random();
// 设定背景色
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
// 设定字体
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
// 随机产生155条干扰线,使图象中的认证码不易被其它程序探测到
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
// 取随机产生的认证码(4位数字)
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(code[random.nextInt(code.length)]);
sRand += rand;
// 将认证码显示到图象中
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
g.drawString(rand, 13 * i + 6, 16);
}
// 赋值验证码
this.str = sRand;
// 图象生效
g.dispose();
this.image = image;/* 赋值图像 */
}
/*
* 给定范围获得随机颜色
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
Controller类:
@Controller
public class LoginController {
//跳转到login(登录页面) GET请求
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String index() {
//如果用户已登录再进就跳转到主页面
User currentUser = UserContext.getUser();
if(currentUser!=null){
return "main";
}
return "login";
}
//Ajax请求,返回数据,不要跳转 {success:true/false,msg:xxx}
// 如果Ajax请求,如果跳转:跳转的页面当前字符串回来/直接报错
//实现登录功能 POST请求
@RequestMapping(value = "/login",method = RequestMethod.POST)
@ResponseBody
public JsonResult login(String name, String password,HttpServletRequest request,HttpServletResponse response) throws Exception{
//判断登录验证码
HttpSession session = request.getSession();
String codeImg = request.getParameter("codeImg");
String code = (String) session.getAttribute("code");
if (!codeImg.equalsIgnoreCase(code)) {
return new JsonResult(false,"验证码错误");
}
//1.拿到当前用户(currentUser)
Subject subject = SecurityUtils.getSubject();
//2.如果当前用户未登录,需要让它登录
if (!subject.isAuthenticated()) {
try {
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
subject.login(token);
} catch (UnknownAccountException e) {
e.printStackTrace();
return new JsonResult(false,"账号出错了!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
return new JsonResult(false,"账号或者密码出错了!");
} catch (AuthenticationException e) {
e.printStackTrace();
//System.out.println("这个错得给钱了,因为我需要找一下!");
return new JsonResult(false,"出现未知错误!请联系管理员!");
}
}
// subject:包含主体,包含会话管理
//把当前登录用户放到Session中去
User user = (User) subject.getPrincipal();
UserContext.setUser(user);
//成功后跳到 main.jsp
return new JsonResult();
}
@RequestMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login";
}
@RequestMapping("/check.jpg")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 通知浏览器不要缓存
response.setHeader("Expires", "-1");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "-1");
CaptchaUtil util = CaptchaUtil.Instance();
// 将验证码输入到session中,用来验证
String code = util.getString();
request.getSession().setAttribute("code", code);
// 输出打web页面
ImageIO.write(util.getImage(), "jpg", response.getOutputStream());
}
4.用户注册的功能实现
1.前台通过form表单的方式,提交注册信息,通过弹窗的方式
html:
<%--用户注册的弹出框--%>
<div id="userDiv" style="display: none;">
<form class="layui-form" id="userForm" lay-filter="userForm" enctype="mutipart/form-data">
<input type="hidden" name="id">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label" >用户名</label>
<div class="layui-input-inline">
<input type="text" name="name" placeholder="6~18位,请使用数字或字母" lay-verify="checkName|username|required" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">密码</label>
<div class="layui-input-inline">
<input type="password" id="password" name="password" lay-verify="required" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-inline">
<input type="password" lay-verify="required|checkPwd" class="layui-input">
</div>
</div>
<%--图片上传功能--%>
<div>
<button type="button" name="url" class="layui-btn" id="test1">上传头像</button>
<img class="layui-upload-img" id="photo" width="80" height="80">
<p id="demoText"></p>
</div>
<hr/>
<div class="layui-form-item" id="btn">
<div class="layui-input-block">
<button id="get" lay-filter="reg1" class="layui-btn btn-submit" type="button" lay-submit="">立即注册</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</div>
</form>
</div>
js部分:
layui.use(['form', 'layer','jquery','upload','element'], function () {
var $ = layui.jquery,form = layui.form,layer = layui.layer,upload = layui.upload,element = layui.element;
// 验证
form.verify({
name: function (value) {
if (value == "") {
return "请输入用户名";
}
},
username:function (value) {
if(value.length<6){
return "用户名最小长度为6";
}
},
password: function (value) {
if (value == "") {
return "请输入密码";
}
},
checkPwd:function(value){
var passwordValue = $("#password").val();
if(value != passwordValue){
return '两次输入的密码不一致!';
}
},
checkName:function (value, item) {
var html = $.ajax({
url: "/user/checkName",
data:{"name":value},
async:false
}).responseText;
if(html=='false'){
return "该用户名已存在";
}
}
});
//注册,打开弹窗
$("#reg").bind("click",function () {
layer.open({
type: 1,
title:'用户注册',
area: ['480px', '400px'],
content: $("#userDiv"),
success: function(layero, index){
var shade=$(".layui-layer-shade");
if(shade.length>1){
$(shade[1]).remove();
}
}
});
})
//注册方法
form.on('submit(reg1)', function (data) {
var fd = new FormData();
var formData = new FormData($("#userForm")[0]);
$.ajax({
cache : true,
type : "post",
url : "/user/reg",
async : true,
data : formData, // 你的formid
contentType: false, //jax 中 contentType 设置为 false 是为了避免 JQuery 对其操作,从而失去分界符,而使服务器不能正常解析文件
processData: false, //当设置为true的时候,jquery ajax 提交的时候不会序列化 data,而是直接使用data
error : function(request) {
layer.alert('操作失败', {
icon: 2,
title:"提示"
});
},
success : function(ret) {
if (ret.success) {
layer.alert('注册成功', {
icon: 2,
title:"提示"
});
layer.closeAll();
window.location.href="/login";
} else {
layer.alert(ret.msg, {
icon: 2,
title:"提示"
});
}
}
})
});
//上传头像
var uploadInst = upload.render({
elem: '#test1' /*根据绑定id,打开本地图片*/
,url: '/user/reg' /*上传后台接受接口*/
,auto: false /*true为选中图片直接提交,false为不提交根据bindAction属性上的id提交*/
,bindAction: '#get'
,drag:true
,choose:function(obj){
//预读本地文件示例,不支持ie8
obj.preview(function(index, file, result){
$('#photo').attr('src', result); //图片链接
});
}
,done: function(res){
console.debug(res)
//如果上传失败
if(res.code > 0){
return layer.msg('上传失败');
}
//上传成功
}
,error: function(){
//演示失败状态,并实现重传
var demoText = $('#demoText');
demoText.html('<span style="color: #FF5722;">上传失败</span> <a class="layui-btn layui-btn-mini demo-reload">重试</a>');
demoText.find('.demo-reload').on('click', function(){
uploadInst.upload();
});
}
});
})
</script>
后台Controller层:
//用户注册的方法
@ResponseBody
@RequestMapping ("/reg")
public JsonResult save(MultipartFile file, User user, HttpServletRequest request){
try {
String path = request.getSession().getServletContext().getRealPath("upload");
String pathPhoto = "/upload";
if(!file.isEmpty()){
String name = file.getOriginalFilename();//获取接受到的图片名称
String newFileName = UUID.randomUUID().toString().substring(0,5)+"."+ FilenameUtils.getExtension(name);
File fi = new File(path,newFileName); //将path路径与图片名称联系在一起
if(!fi.getParentFile().exists()){ //判断是否存在path路径下的文件夹
fi.getParentFile().mkdirs(); //不存在创建path路径下的文件夹
}
file.transferTo(fi); //上传图片
user.setImgurl(pathPhoto+"/"+newFileName); //为保存图片路径
}
if(!StringUtil.isEmpty(user.getName()) && !StringUtil.isEmpty(user.getPassword())){
userService.save(user);
}
} catch (Exception e) {
e.printStackTrace();
return new JsonResult(false,e.getMessage());
}
return new JsonResult();
}
5.登录用户的crud,主要对于登录用户的角色管理
1.mysql表:
t_user:用户表,有用户的账号密码以及头像信息
user_role:用户与角色的中间表,主要配置用户与角色之间的关联
t_role表:角色表
2.前台页面的展示,通过数据表格展示用户信息,以及用户对应的角色信息:
3.这里使用到了layui的分页功能,需要使后台传递的数据与前台需要接收的数据格式保持一致才能实现
–创建一个分页的工具类
public class PageUi<T> {
private Integer code = 0;
private String msg;
private Long count;
private List<T> data = new ArrayList<>();
//get set省略
–在baseService中添加方法,让后台传递的数据与前台需要的数据保持一致
@Override
public PageUi<T> findByQuery(BaseQuery query) {
PageUi<T> pageUi = new PageUi<>();
Page page = PageHelper.startPage(query.getPage(), query.getLimit());
//查询当前页的数据
Page<T> data = (Page<T>) getMapper().findByPage(query);
//获取总条数
long count = page.getTotal();
pageUi.setCount(count);
pageUi.setData(data);
return pageUi;
}
–创建BaseQuery类,主要用于高级查询的功能
public class BaseQuery {
private Integer page = 1;
private Integer limit = 10;
private String q;
//get set省略
}
4.UserMapper持久层,需要在xml中添加业务需要的sql,注意在xml中配置以后,在mapper类中添加相应的方法,并且在业务层调用方法
<?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="cn.itsource.rpms.mapper.UserMapper" >
<resultMap id="BaseResultMap" type="cn.itsource.common.domain.User" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="password" property="password" jdbcType="VARCHAR" />
<result column="imgurl" property="imgurl" jdbcType="VARCHAR" />
<result column="openid" property="openid" jdbcType="VARCHAR" />
<collection property="roles" column="id" select="cn.itsource.rpms.mapper.UserMapper.findByRole">
</collection>
<collection property="menus" column="id" select="cn.itsource.rpms.mapper.UserMapper.findByMenu">
</collection>
</resultMap>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long" >
delete from t_user
where id = #{id,jdbcType=BIGINT}
</delete>
<insert id="insert" parameterType="cn.itsource.common.domain.User" >
insert into t_user (id, name, password,
imgUrl,openid)
values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR},
#{imgurl,jdbcType=VARCHAR},#{openid,jdbcType=VARCHAR})
</insert>
<update id="updateByPrimaryKey" parameterType="cn.itsource.common.domain.User" >
update t_user
set name = #{name,jdbcType=VARCHAR},
password = #{password,jdbcType=VARCHAR},
imgUrl = #{imgurl,jdbcType=VARCHAR},
openid = #{openid,jdbcType=VARCHAR}
where id = #{id,jdbcType=BIGINT}
</update>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long" >
select id, name, password, imgUrl
from t_user
where id = #{id,jdbcType=BIGINT}
</select>
<select id="selectAll" resultMap="BaseResultMap" >
select id, name, password, imgUrl
from t_user
</select>
<!--登录查询用户名-->
<select id="findByName" parameterType="java.lang.String" resultMap="BaseResultMap" >
select * from t_user where name=#{_parameter}
</select>
<!--用户名验证-->
<select id="checkName" parameterType="java.lang.String" resultMap="BaseResultMap" >
select * from t_user where name=#{_parameter}
</select>
<!--通过id查询-->
<select id="findById" parameterType="java.lang.Long" resultMap="BaseResultMap" >
select * from t_user where id=#{id}
</select>
<!--分页高级查询-->
<select id="findByPage" parameterType="cn.itsource.common.query.UserQuery" resultMap="BaseResultMap" >
select * from t_user
<where>
<if test="name!=null and name !=''">
AND name LIKE CONCAT("%",#{name},"%")
</if>
</where>
</select>
<!--通过用户拿到角色-->
<select id="findByRole" resultType="cn.itsource.common.domain.Role" >
SELECT r.* FROM t_user u
left join user_role ur on u.id = ur.user_id
left join t_role r on ur.role_id =r.id
where u.id=#{id}
</select>
<!--通过openid查询-->
<select id="findByOpenid" parameterType="java.lang.String" resultMap="BaseResultMap" >
select * from t_user where openid=#{_parameter}
</select>
</mapper>
5.Controller层的实现
注意maybatis没有级联删除,所以在修改或者删除数据的时候,需要对中间表也进行相应的删除或者添加
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
//注入user和角色中间表
@Autowired
private IUserRoleService userRoleService;
@Autowired
private IRoleService roleService;
@RequestMapping("/index")
public String index(){
return "user/user";
}
//分页展示
@RequestMapping("/findByPage")
@ResponseBody
public PageUi<User> findByPage(UserQuery query) {
return userService.findByQuery(query);
}
//修改
@RequestMapping("/update")
@ResponseBody
public JsonResult update(User user){
//通过id拿到数据库中对应的用户
User currentUser = userService.findOne(user.getId());
//通过id清除用户角色中间表
userRoleService.deleteByUserId(user.getId());
//拿到角色的集合
List<Role> rolesId = user.getRoles();
//通过id拿到所有角色
List<Role> roles =new ArrayList<>();
for (Role e : rolesId) {
Role role = roleService.findOne(e.getId());
roles.add(role);
//重新关联中间表
UserRole userRole=new UserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(e.getId());
userRoleService.save(userRole);
}
//封装角色
currentUser.setRoles(roles);
try {
userService.update(currentUser);
} catch (Exception e) {
e.printStackTrace();
return new JsonResult(false,e.getMessage());
}
return new JsonResult();
}
//删除
@RequestMapping("/delete")
@ResponseBody
public JsonResult delete(Long[] ids) {
try {
for (Long id : ids) {
userService.delete(id);
//通过id清除用户角色中间表
userRoleService.deleteByUserId(id);
}
} catch (Exception e) {
e.printStackTrace();
return new JsonResult(false,e.getMessage());
}
return new JsonResult();
}
//用户名重复验证
@RequestMapping("/checkName")
@ResponseBody
public boolean checkName(String name,Long id){
return userService.CheckName(name,id);
}
}
6.前台页面展示
修改的弹窗,并且实现对用户角色回显功能,这里使用到了layui的扩展组件tableSelected
jsp代码
<!doctype html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="utf-8">
<title>layui</title>
<link rel="stylesheet" href="/static/js/layui/css/layui.css" media="all">
<!-- 注意:如果你直接复制所有代码到本地,上述css路径需要改成你本地的 -->
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/jquery.jdirk.js"></script>
<script src="/static/js/jquery.form.min.js"></script>
<script src="/static/js/layui/layui.js" charset="utf-8"></script>
</head>
<body>
<%--添加修改数据的表单--%>
<div id="userDiv" style="display: none">
<form class="layui-form" id="roleForm" lay-filter="roleForm" action="">
<%--id--%>
<input type="hidden" name="id"/>
<div class="layui-form-item" style="padding-top: 9px;">
<label class="layui-form-label">用户名</label>
<div class="layui-input-inline">
<input type="text" name="name" required lay-verify="required|username|checkName" autocomplete="off"
class="layui-input">
</div>
</div>
<%--角色下拉框--%>
<div class="layui-form-item">
<label class="layui-form-label">角色管理</label>
<div class="layui-input-inline">
<input type="text" placeholder="请选择" autocomplete="off" class="layui-input" id="demo" ts-selected="">
</div>
</div>
<%--提交重置按钮--%>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" type="button" lay-submit lay-filter="formDemo" data-type="save">立即提交</button>
<button type="reset" class="layui-btn layui-btn-primary" data-type="reset">重置</button>
</div>
</div>
</form>
</div>
<%--高级查询表单--%>
<div class="demoTable">
<form id="sreachForm">
角色名称:
<div class="layui-inline">
<input class="layui-input" name="name" autocomplete="off">
</div>
<%--button如果在form表单内部,它默认点击是submit提交--%>
<button class="layui-btn" type="button" data-type="search">搜索</button>
</form>
</div>
<%--数据表格--%>
<table class="layui-hide" id="userTable"></table>
<%--增删改查按钮--%>
<script type="text/html" id="toolbarDemo">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm layui-btn-disabled" data-type="add">添加</button>
<button class="layui-btn layui-btn-sm" data-type="delete">删除</button>
<button class="layui-btn layui-btn-sm" data-type="update">编辑</button>
</div>
</script>
<!-- 注意:如果你直接复制所有代码到本地,上述js路径需要改成你本地的11 -->
<script src="/static/js/model/user/user.js"></script>
</body>
</html>
js代码
layui.config({
base: '/static/js/'
}).extend({
tableSelect:'./layui_extends/tableSelect',
}).use(['table', 'jquery', 'layer', 'form','tableSelect'], function () {
// 对需要的组件进行挂载
var table = layui.table;
var $ = layui.jquery;
var layer = layui.layer;
var form = layui.form;
var tableSelect = layui.tableSelect;
//开始使用
tableSelect.render({
elem: '#demo', //定义输入框input对象 必填
checkedKey: 'id', //表格的唯一建值,非常重要,影响到选中状态 必填
searchKey: 'keyword', //搜索输入框的name值 默认keyword
searchPlaceholder: '关键词搜索', //搜索输入框的提示文字 默认关键词搜索
table: { //定义表格参数,与LAYUI的TABLE模块一致,只是无需再定义表格elem
url:'/role/findByPage',
cols: [[
{ type: 'checkbox' }
,{field: 'id', title: 'ID', sort: true}
, {field: 'sn', title: '角色名称', sort: true}
, {field: 'permissions', title: '角色拥有权限', sort: true,
templet: function (d) {
var roles=[];
if(d.permissions){
$.each(d.permissions,function (index,obj) {
if(obj){
roles.push(obj.name)
}
})
return roles.length>0?roles:"无";
}
}
}
]],page:true
},
done: function (elem, data) {
var NEWJSON = [];
layui.each(data.data, function (index, item) {
NEWJSON.push(item.name)
})
elem.val(NEWJSON.join(","));
}
})
table.render({
elem: '#userTable'
, url: '/user/findByPage'
,even: true
// 自定义按钮
, toolbar: "#toolbarDemo"
, cellMinWidth: 80 //全局定义常规单元格的最小宽度,layui 2.2.1 新增
, cols: [[
{type: 'checkbox'}
, {field: 'name', title: '用户名',sort: true, align: 'center'}
, {
field: 'imgurl', title: '头像',
templet: function (d) {
return "<img id='imgHead' src='" + d.imgurl + "' alt='无头像' width='28'/>";
}
, align: 'center'}
,{
field: 'roles', title: '对应角色', align: 'center',
templet: function (d) {
var roles=[];
if(d.roles){
$.each(d.roles,function (index,obj) {
if(obj){
roles.push(obj.name)
}
})
return roles.length>0?roles:"无";
}
}
}
]]
, page: true,id:'userList'
//数据渲染完毕之后,执行done函数内部的代码
, done() {
//给按钮注册点击事件
$(".layui-btn").on("click", function () {
var methodName = $(this).data("type");
obj[methodName]();
});
}
});
//方法
var obj={
//清除用户
delete:function () {
// 获取选中的数据
var checkStatus = table.checkStatus("userList");
//数组,所有行的数据
var rows = checkStatus.data;
if (!rows.length) {
layer.alert("请选中数据再进行删除", {
title: "提示",
icon: 2
});
return;
}
//如果存在,将选中的数据的id抽出来,装进数组
var ids = [];
for (let row of rows) {
ids.push(row.id);
}
layer.confirm('你确认清除该用户吗?', {icon: 3, title: '提示'}, function (index) {
$.post("/user/delete", {"ids": ids.toString()}, function (data) {
if (data.success) {
layer.alert('删除成功', {
icon: 1,
title:"提示"
});
table.reload("userList");
//自动关闭弹出层
layer.close(index);
} else {
layer.alert(data.msg, {
icon: 2,
title: '错误'
})
}
})
})
},
//修改保存
update:function () {
// 获取选中的数据
var checkStatus = table.checkStatus("userList");
//没有选中,给提示
var rows = checkStatus.data;
if (!rows.length) {
layer.alert("请选中数据再进行删除", {
title: "提示",
icon: 2
});
return;
}
//清空表单
$("#userDiv").clearForm();
//打开form表单
layer.open({
type: 1,
title:'编辑用户',
area: ['420px', '350px'],
content: $("#userDiv"),
success: function(layero, index){
var shade=$(".layui-layer-shade");
if(shade.length>1){
$(shade[1]).remove();
}
}
});
//数据回显
var ids = []
$.each(rows,function (index,obj){
ids.push(rows[index].id)
})
var row=null;
var rowArr=[];
var idArr=[];
console.debug(rows[0].roles[0])
if(rows[0].roles[0]){
$.each(ids,function (index,obj) {
row=rows[index];
$.each(row.roles,function (index,obj) {
rowArr.push(row.roles[index].name);
idArr.push(row.roles[index].id);
})
$("#demo").val(rowArr);
$("#demo").attr("ts-selected",idArr)
})
}
form.val(("roleForm"),rows[0])
},
//编辑用户的保存
save:function () {
form.on('submit(formDemo)', function(data){
var roleArr=[]
if($("#demo").attr("ts-selected")){
roleArr = ($("#demo").attr("ts-selected")).split(",");
}
var params= data.field;
roleArr=roleArr.map(Number);
for (var i = 0; i < roleArr.length; i++) {
//动态的拼接传参 菜单
params[`roles[${i}].id`] = roleArr[i]
}
$.post("/user/update",params,function (result) {
if (result.success) {
//提示用户数据添加成功
layer.msg("数据修改成功!");
//关闭所有弹出层
layer.closeAll();
//刷新表格
table.reload("userList");
} else {
layer.alert(result.msg, {
"title": "错误",
"icon": 2
});
}
})
});
}
}
});
6.第三方登录功能实现
1.使用微信方式登录
微信二维码生成的页面,通过申请微信公众平台账号,审核通过获得,这里不作讲解
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>微信登录</title>
<style type="text/css">
iframe{width: 100%;height: 100%}
</style>
</head>
<body>
<div style="margin-top: 10%;" align="center">
<div id="login_container">
</div>
</div>
<%--引入微信的js支持--%>
<script type="text/javascript" src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
<script type="text/javascript">
var obj = new WxLogin({
self_redirect:false,//为false表示是PC端网页
id:"login_container",
appid: "自己的",
scope: "snsapi_login",
redirect_uri: "自己的主页面",
state: "xxx",
style: "white",
});
</script>
</body>
</html>
2.导入需要用到的jar包,配置pom.xml
<!--json格式转换包-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!--第三方登录发送请求包-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
3.登录页面点击按钮跳转到微信二维码页面,用户通过扫描二维码,可以获得当前用户的信息,其中有个openid是我们需要的,在数据库用户表中添加openid字段,并且通过openid判断扫描用户是否已经绑定了微信账号
代码实现:
@Controller
@RequestMapping("/wechat")
public class WechatController {
@Autowired
private IUserService userService;
@RequestMapping("/login")
public String login(){
return "login1";
}
@RequestMapping("/callback")
public String callback(String code, String state, Model model, HttpServletRequest req){
String atUrl = WxConstants.ACCESSTOKEURL.replace("APPID", WxConstants.APPID)
.replace("SECRET",WxConstants.APPSECRET)
.replace("CODE",code);
String atJsonStr = HttpClientUtil.doGet(atUrl);
// System.out.println("atJsonStr:"+atJsonStr);
JSONObject jsonObject = (JSONObject)JSON.parse(atJsonStr);
String access_token = String.valueOf(jsonObject.get("access_token"));
String open_id = String.valueOf(jsonObject.get("openid"));
// System.out.println("access_token:"+access_token);
// System.out.println("open_id:"+open_id);
String userInfoUrl = WxConstants.USERINFOURL.replace("ACCESS_TOKEN", access_token).replace("OPENID", open_id);
String userInfo = HttpClientUtil.doGet(userInfoUrl);
//登录用户的所有信息
JSONObject userJson = (JSONObject)JSON.parse(userInfo);
// System.out.println(userJson);
//完成绑定操作
// model.addAttribute("userInfo", userInfo);
//从三方登录的微信中获取用户信息
String name = String.valueOf(userJson.get("nickname"));
String imgurl = String.valueOf(userJson.get("headimgurl"));
String openid = String.valueOf(userJson.get("openid"));
//通过openid查询是否绑定用户
User user = userService.findByOpenid(openid);
Subject subject = SecurityUtils.getSubject();
//如果用户存在,就将用户存入session中
if(user!=null){
UsernamePasswordToken token = new UsernamePasswordToken(user.getName(),user.getPassword());
if(!subject.isAuthenticated()){
subject.login(token);
UserContext.setUser(user);
}
return "main";
}else{
//绑定账号
//将opendid存入session中
req.getSession().setAttribute("openid", openid);
//如果不存在就跳转到注册页面
return "wechat";
}
}
}
4.在这里有一个问题,在配置了shiro拦截之后,如果要将密码放入token之中,那么会对密码再进行一次加密处理,此时的密码加密了两次,而数据库中的密码只加密了一次。解决办法,自定义了一个HashedCredentialsMatcher实现类,用于判断token,这里假设如果登录密码是32为就是正确的密码
public class CybHashedCredentialsMatcher extends HashedCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {
UsernamePasswordToken token=(UsernamePasswordToken)authcToken;
String pwd = String.valueOf(token.getPassword());
Object accountCredentials = getCredentials(info);
if(pwd.length() == 32){
return true;
}
//将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false
return super.doCredentialsMatch(token, info) ;
}
}
注意这个类需要在shiro.xml中引入
5.如果在数据库中没有查询到扫码用户对应是账号,我们给出了两个选择,一个是绑定数据库中已有的账号,通过输入用户名来绑定,另一个是绑定新账号,并且在绑定之后,用户应当直接跳转到主页面。
–5.1绑定已有的账号
此时不管没有没账号,我们已经得到了扫码用户的openid了,那么将openid存入session中,并且将openid赋值给数据库查询出的某一条数据,就可以实现绑定数据库已有的账号,通过隐藏域的方式提交openid,
jsp页面:
<div class="login">
<h1>微信绑定已有账号</h1>
<div id="userDiv">
<form class="layui-form" id="userForm" lay-filter="userForm">
<%--用于提交openid--%>
<input type="hidden" name="openid" value="${openid}">
<div class="layui-form-item">
<div class="layui-form-item">
<input type="text" name="name" placeholder="请输入要绑定的用户名" lay-verify="username|required" class="layui-input">
</div>
<hr/>
<button type="button" id="get" class="layui-btn login_btn" lay-submit="" lay-filter="reg1">立即绑定</button>
</div>
</form>
</div>
</div>
<%--用户注册的弹出框--%>
<script type="text/javascript">
layui.use(['form', 'layer','jquery','upload','element'], function () {
var $ = layui.jquery,form = layui.form,layer = layui.layer,upload = layui.upload,element = layui.element;
// 验证
form.verify({
name: function (value) {
if (value == "") {
return "请输入用户名";
}
},
username:function (value) {
if(value.length<6){
return "用户名最小长度为6";
}
},
});
//注册方法
form.on('submit(reg1)', function (data) {
var userData = data.field;
$.get("/user/regWechat",userData,function(data){
if (data.success) {
layer.alert('绑定成功', {
icon: 2,
title:"提示"
});
layer.closeAll();
window.location.href="/login";
} else {
layer.alert(data.msg, {
icon: 2,
title:"提示"
});
}
})
});
//回车登录功能
$(document.documentElement).on("keyup", function(event) {
var keyCode = event.keyCode;
if (keyCode === 13) { // 捕获回车
$("#get").trigger("click");
}
});
})
</script>
</body>
</html>
后台UserController
//绑定已有的账户
@RequestMapping("/regWechat")
@ResponseBody
public JsonResult regWechat(User userData){
User user = userService.findByName(userData.getName());
if(user==null){
return new JsonResult(false,"输入的用户不存在");
}
user.setOpenid(userData.getOpenid());
userService.update(user);
//绑定之后直接跳转到主页面
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getName(),user.getPassword());
if(!subject.isAuthenticated()){
subject.login(token);
UserContext.setUser(user);
}
return new JsonResult();
}
–5.2绑定新用户
jsp页面:
<div class="login">
<h1>微信绑定新账号</h1>
<div id="userDiv">
<form class="layui-form" id="userForm" lay-filter="userForm" enctype="mutipart/form-data">
<%--用于提交openid--%>
<input type="hidden" name="openid" value="${openid}">
<div class="layui-form-item">
<div class="layui-form-item">
<input type="text" name="name" placeholder="6~18位,请使用数字或字母" lay-verify="checkName|username|required" class="layui-input">
</div>
<div class="layui-form-item">
<input type="password" id="password" placeholder="密码" name="password" lay-verify="required" class="layui-input">
</div>
<div class="layui-form-item">
<input type="password" lay-verify="required|checkPwd" placeholder="重复密码" class="layui-input">
</div>
<%--图片上传功能--%>
<div>
<button type="button" name="url" class="layui-btn" id="test1">上传头像</button>
<img class="layui-upload-img" id="photo" width="80" height="80">
<p id="demoText"></p>
</div>
<hr/>
<button type="button" id="get" class="layui-btn login_btn" lay-submit="" lay-filter="reg1">立即绑定</button>
</form>
</div>
</div>
<%--用户注册的弹出框--%>
<script type="text/javascript">
layui.use(['form', 'layer','jquery','upload','element'], function () {
var $ = layui.jquery,form = layui.form,layer = layui.layer,upload = layui.upload,element = layui.element;
// 验证
form.verify({
name: function (value) {
if (value == "") {
return "请输入用户名";
}
},
username:function (value) {
if(value.length<6){
return "用户名最小长度为6";
}
},
password: function (value) {
if (value == "") {
return "请输入密码";
}
},
checkPwd:function(value){
var passwordValue = $("#password").val();
if(value != passwordValue){
return '两次输入的密码不一致!';
}
},
checkName:function (value, item) {
var html = $.ajax({
url: "/user/checkName",
data:{"name":value},
async:false
}).responseText;
if(html=='false'){
return "该用户名已存在";
}
}
});
//注册方法
form.on('submit(reg1)', function (data) {
var fd = new FormData();
var formData = new FormData($("#userForm")[0]);
$.ajax({
cache : true,
type : "post",
url : "/user/reg",
async : true,
data : formData, // 你的formid
contentType: false, //jax 中 contentType 设置为 false 是为了避免 JQuery 对其操作,从而失去分界符,而使服务器不能正常解析文件
processData: false, //当设置为true的时候,jquery ajax 提交的时候不会序列化 data,而是直接使用data
error : function(request) {
layer.alert('操作失败', {
icon: 2,
title:"提示"
});
},
success : function(ret) {
if (ret.msg&&ret.success) {
layer.alert('注册绑定成功', {
icon: 2,
title:"提示"
});
layer.closeAll();
window.location.href="/main/index";
} else {
layer.alert(ret.msg, {
icon: 2,
title:"提示"
});
}
}
})
});
//上传头像
var uploadInst = upload.render({
elem: '#test1' /*根据绑定id,打开本地图片*/
,url: '/user/reg' /*上传后台接受接口*/
,auto: false /*true为选中图片直接提交,false为不提交根据bindAction属性上的id提交*/
,bindAction: '#get'
,drag:true
,choose:function(obj){
//预读本地文件示例,不支持ie8
obj.preview(function(index, file, result){
$('#photo').attr('src', result); //图片链接
});
}
,done: function(res){
console.debug(res)
//如果上传失败
if(res.code > 0){
return layer.msg('上传失败');
}
//上传成功
}
,error: function(){
//演示失败状态,并实现重传
var demoText = $('#demoText');
demoText.html('<span style="color: #FF5722;">上传失败</span> <a class="layui-btn layui-btn-mini demo-reload">重试</a>');
demoText.find('.demo-reload').on('click', function(){
uploadInst.upload();
});
}
});
//回车登录功能
$(document.documentElement).on("keyup", function(event) {
var keyCode = event.keyCode;
if (keyCode === 13) { // 捕获回车
$("#get").trigger("click");
}
});
})
</script>
后台UserController
在用户的注册的方法里,添加判断条件,判断openid是否存在,如果存在就视为扫码注册
//如果是三方登录创建,直接跳转到主页面
if(user.getOpenid()!=null){
UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), MD5Util.createMd5Str( user.getPassword()));
Subject subject = SecurityUtils.getSubject();
if(!subject.isAuthenticated()){
subject.login(token);
}
user = (User)subject.getPrincipal();
UserContext.setUser(user);
return new JsonResult(true,"weixin");
}
–5.3功能优化
为每个新注册的用户设计一个默认角色,在新增保存的时候执行代码
//密码加密的方法
@Override
@Transactional
public void save(User user) {
//判断是修改保存还是新增保存
if(user.getId()==null){
user.setPassword(MD5Util.createMd5Str(user.getPassword()));
}
userMapper.insert(user);
//为每个注册的用户设置默认角色
User currentUser = userMapper.findByName(user.getName());
Role role = roleService.selectRoleByName("基本用户");
UserRole userRole = new UserRole();
userRole.setUserId(currentUser.getId());
userRole.setRoleId(role.getId());
userRoleService.save(userRole);
}
}