https://start.aliyun.com
Thymeleaf
在Template Engines
中因为还要适配移动端的页面,因此选择amazeui作为基本样式库
assets
复制到resources/static
目录下resources/static/assets/js
下,也可在我的github工程中下载resources/templates
下index.css
和tig.css
两个样式文件到resources/static/style/js
下,其中tig.css设置初始时“用户名或密码错误”和“客官,密码格式6-18位”两个提示不显示login.js
和background.js
到resources/static/style/js
下,login.js
主要作用为判断账号和密码是否符合有关的判断,显示有关提示background.js
是背景有关的,实现背景的线条吸附和点按出现小心心resources/style/images
下,命名为myPicture.jpg
com.liang.modules.sys.controller
包@Controller
public class TestController {
@RequestMapping("/")
public String toLogin(){
return "login";
}
}
创建数据库,并依次创建用户信息表、角色表、权限表、角色权限映射表
create database blog1;
use blog1;
# 创建用户信息表
create table users(
`id` int(11) not null auto_increment comment 'id',
`username` varchar(255) not null comment '用户名',
`password` varchar(60) not null comment '密码',
`phone` varchar(11) not null comment '手机',
`last_time` date default null comment '最后登录时间',
`role_id` int(1) not null comment '角色id',
primary key (`id`)
)engine=InnoDB DEFAULT CHARSET=utf8mb4;
# 创建角色表
create table roles(
`id` int(1) not null comment '角色id',
`role_name` varchar(10) not null comment '角色名称',
primary key (`id`)
)engine=InnoDB DEFAULT CHARSET=utf8mb4;
# 创建权限表
create table permission(
`id` int(1) not null comment '权限id',
`permission_name` varchar(10) not null comment '权限名称',
primary key(`id`)
)engine=InnoDB DEFAULT CHARSET=utf8mb4;
# 创建角色和权限对应表
create table role_permission (
`role_id` int(1) not null comment '角色id',
`permission_id` int(1) not null comment '权限id',
key `role_id` (`role_id`),
key `permission_id` (`permission_id`)
)engine=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入角色表信息
insert into roles values (1, 'admin');
insert into roles values (2, 'user');
insert into roles values (3, 'partner');
# 插入权限表信息
insert into permission values (1, 'editor');
insert into permission values (2, 'manage');
insert into permission values (3, 'comment');
#插入角色和对应的权限对应关系
insert into role_permission values (1, 2);
insert into role_permission values (2, 3);
insert into role_permission values (3, 1);
# 插入用户信息
insert into users values(1, '洪城布衣','123456', '12345678910', '2021-12-22', 1);
select u.*, r.role_name, rpn.permission_id, p.permission_name from users u
join roles r on u.role_id = r.id
join role_permission rpn on r.id = rpn.role_id
join permission p on rpn.permission_id = p.id;
在pom.xml的标签中添加如下
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.14version>
dependency>
添加后,版本号或包会是红色,点Maven->再刷新,将会对相应的依赖进行下载和加载,加载完成后红色就消失,当添加其它依赖时也是这样操作,一直等待其红色消失
# 使用application的配置文件
spring:
# 配置文件 dev|test|prod
profiles:
active: dev
# mybatis-plus配置
mybatis-plus:
mapper-locations: classpath:mappering/*.xml
#实体扫描,多个package用逗号或者分号分隔
type-aliases-package: com.liang.modules.sys.entity
global-config:
db-config:
table-underline: true
db-type: mysql
server:
port: 8080
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/blog1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
filter:
commons-log:
connection-logger-name: stat,wall,log4j
max-pool-prepared-statement-per-connection-size: 20
use-global-data-source-stat: true
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
@Data
@TableName(value = "users")
public class UsersEntity implements Serializable {
private static final long serialVersionUID = 4183054704913917874L;
@TableId("id")
private long id;
private String username;
private String password;
private String phone;
private String lastTime;
private Integer roleId;
}
@Data
@TableName(value = "permission")
public class PermissionEntity implements Serializable {
private static final long serialVersionUID = 7501360721677209850L;
private int id;
private String permissionName;
}
@Data
@TableName(value = "roles")
public class RoleEntity implements Serializable {
private static final long serialVersionUID = 7840592564845432616L;
private int id;
private String roleName;
}
@Data
@TableName(value = "roles")
public class RoleVOEntity extends RoleEntity implements Serializable {
private static final long serialVersionUID = 3137462801862176163L;
@TableField(exist = false)
private Set<PermissionEntity> permissionSet;
}
@Data
@TableName(value = "users")
public class UsersVOEntity extends UsersEntity implements Serializable {
private static final long serialVersionUID = -8860349534536707832L;
@TableField(exist = false)
private Set<RoleVOEntity> roles;
}
alt+Insetrt
自动生成序列化uid@Repository
public interface UserDao extends BaseMapper<UsersEntity> {
}
@MapperScan("com.liang.modules.sys.dao")
com.liang.TestBlog01ApplicationTests
中,填写测试内容,测试类所在位置可看下一步的测试@SpringBootTest
class TestBlog01ApplicationTests {
@Autowired
private UserDao userDao;
@Test
void contextLoads() {
List<UsersEntity> usersEntities = userDao.selectList(null);
System.out.println(usersEntities);
}
}
UsersLoginDao接口、UsersLoginDao.xml、UserService接口以及UserServiceImpl实现类
@Repository
public interface UserLoginDao extends BaseMapper<UsersVOEntity> {
UsersVOEntity findByPhone(String phone);
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liang.modules.sys.dao.UserLoginDao">
<resultMap id="userMap" type="com.liang.modules.sys.entity.VO.UserVOEntity">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="phone" column="phone"/>
<result property="lastTime" column="last_time"/>
<collection property="roles" ofType="com.liang.modules.sys.entity.VO.RoleVOEntity">
<id property="id" column="role_id"/>
<result property="roleName" column="role_name"/>
<collection property="permissionSet" ofType="com.liang.modules.sys.entity.PermissionEntity">
<id property="id" column="permission_id"/>
<result property="permissionName" column="permission_name"/>
collection>
collection>
resultMap>
<select id="findByPhone" parameterType="String" resultMap="userMap">
select u.*, r.role_name, p.id as permission_id, p.permission_name
from users u
join roles r on u.role_id=r.id
join role_permission rp on r.id = rp.role_id
join permission p on p.id = rp.permission_id
where phone=#{phone};
select>
mapper>
@Autowired
private UserLoginDao userLoginDao;
@Test
void test2(){
UserVOEntity byPhone = userLoginDao.findByPhone("12345678910");
System.out.println(byPhone.getUsername());
System.out.println(byPhone.getPassword());
System.out.println("====>Roles");
byPhone.getRoles().forEach(x-> System.out.println(x.getId()+" "+x.getRoleName()+ x.getPermissionSet()));
}
com.liang.modules.sys.service
包com.liang.modules.sys.service.UserService
接口和com.liang.modules.sys.service.impl.UserServiceImpl
实现类public interface UserService {
UserVOEntity findByPhone(String phone);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserLoginDao userLoginDao;
@Override
public UserVOEntity findByPhone(String phone) {
return userLoginDao.findByPhone(phone);
}
}
@Autowired
private UserServiceImpl userServiceImpl;
@Test
void test3(){
UserVOEntity byPhone = userServiceImpl.findByPhone("12345678910");
System.out.println(byPhone.getUsername());
System.out.println(byPhone.getPassword());
System.out.println("====>Roles");
byPhone.getRoles().forEach(x-> System.out.println(x.getId()+" "+x.getRoleName()+ x.getPermissionSet()));
}
pom.xml中添加
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-web-starterartifactId>
<version>1.7.1version>
dependency>
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.0.0version>
dependency>
创建com.liang.modules.sys.shiro.UserRealm
类
暂时重写认证方法,主要内容为通过token获取到网页传来的username(其实为手机号)与密码加盐后生成编码,即内容暂时为
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
UserVOEntity user = userService.findByPhone(userToken.getUsername());
if (user==null){
throw new UnknownAccountException("该手机号不存在!");
} else {
// 以手机号作为盐值进行MD5加盐
ByteSource salt = ByteSource.Util.bytes(user.getPhone());
return new SimpleAuthenticationInfo(user, user.getPassword(), salt, this.getName());
}
}
}
创建com.liang.modules.sys.shiro.ShiroConfig
@Configuration
public class ShiroConfig {
/**
* 密码校验规则 HashedCredentialsMatcher
* 这个Bean自动装载到Spring中,当登录认证的时候自动使用这种方式对密码进行编码,因为首先密码不会在数据库中明文保存
*/
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//指定加密方式为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
//加密次数
credentialsMatcher.setHashIterations(1024);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(
@Qualifier("defaultWebSecurityManager") WebSecurityManager securityManager
){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager); // 设置安全管理器
// 设置登录url映射
bean.setLoginUrl("login");
// 设置未授权时的跳转的请求
bean.setUnauthorizedUrl("/");
// 添加shiro的内置过滤器
/*
anon: 无需认证就可以登录
authc:必须认证才能登录
user: 必须拥有“记住我”这个功能
perms:拥有对某个资源的权限才能访问
role:拥有某个角色才能访问
*/
LinkedHashMap<String, String> filterMap = new LinkedHashMap<>(); //使用LinkedHashMap可以保证顺序 以便 /** anon在最后过滤
// 权限授权,访问url需要权限,支持通配符
filterMap.put("/", "anon");
filterMap.put("/user", "authc"); // authc -- 认证(登录)才能使用
filterMap.put("/editor", "roles[admin]");
filterMap.put("/SuperAdmin", "roles[admin]");
filterMap.put("/druid/**", "anon");
filterMap.put("/**", "anon");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
//2. 获取安全管理器
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(
@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm); //注入
return securityManager;
}
// 1.定义userRealm进springboot组件 并配置认证加密方式
@Bean(name="userRealm")
public UserRealm userRealm(){
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return userRealm;
}
}
创建com.liang.common.utils.JsonResult
类
@Data
@AllArgsConstructor
public class JsonResult {
private Integer status; //业务相应状态
private String msg; //响应消息
private Object data; //响应数据
}
创建com.liang.modules.sys.controller.LoginController
该部分代码主要完成登陆的验证跳转,具体流程可看后面的流程分析
@RestController
public class LoginController {
@GetMapping("loginUser")
public JsonResult loginUser(@RequestParam("phone") String phone,
@RequestParam("password") String password,
HttpSession session){
UsernamePasswordToken token = new UsernamePasswordToken(phone, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
UserVOEntity user = (UserVOEntity) subject.getPrincipal();
user.setPassword("");
session.setAttribute("user", user);
return new JsonResult(200, "ok", null);
} catch (AuthenticationException e){
return new JsonResult(500, "用户名或密码错误", null);
}
}
}
创建类
public class ShiroMD5 {
public static Object MD5(String phone, String password){
// 与UserRealm中的认证方法一样,以手机号作为盐值进行MD5加盐
ByteSource salt = ByteSource.Util.bytes(phone);
// 参数分别是加密方式、待加密串(密码)、盐、加密次数
// 这里的加密方式和次数要与ShiroConfig中我们写的hashedCredentialsMatcher一致
return new SimpleHash("MD5", password, salt, 1024);
}
public static void main(String[] args) {
// 保存密码到数据库的时候,可以将手机号、密码输入进来,这将生成一个加密后的字符串,
// 将生成的加密字符串保存至数据库即可,下次登陆时将使用加密串进行核验
System.out.println(MD5("12345678910", "123456"));
}
}
运行main函数,我得到的是f878b04f4dcf4610626327d0630fc81f
在博客使用的数据库中执行如下指令更新数据库
update users
set password='f878b04f4dcf4610626327d0630fc81f'
where phone='12345678910';
这里由于我们使用的手机号是12345678910,其实是没有这种12开头规范的手机号,我们将resources/static/style/js/login.js
中的myreg正则表达式更改为可以匹配我们设定的手机号的
即将log函数中这个
var myreg = /^(((13[0-9]{1})|(14[0-9]{1})|(17[0]{1})|(15[0-3]{1})|(15[5-9]{1})|(18[0-9]{1}))+\d{8})$/;
改为
var myreg = /^(((12[0-9]{1})|(13[0-9]{1})|(14[0-9]{1})|(17[0]{1})|(15[0-3]{1})|(15[5-9]{1})|(18[0-9]{1}))+\d{8})$/;
我在做到这里的时候出现了依赖错误,具体情况是程序报红但点进去后又不爆红了,不爆红后点运行,出现org.apache.shiro.spring.web不存在(如下图),但点进程序段又没爆红,最后是通过配置使用其他maven解决的,没出现问题可以继续往后进行
运行主启动类,即com.liang.TestBlog01Application
输入网址http://localhost:8080/
输入手机12345678910
输入密码,先随机输入一个错误的密码,例如123123,登录
提示密码错误
再输入123456,跳转到另外一个界面,并且浏览器地址栏显示为http://localhost:8080/lasturl
,证明代码到此就完成了登陆的认证操作
login.html
中被加载,插入的为
login.js
中定义了两个变量,分别为qz-login
样式和notice-box
样式的元素选择器,其中的样式可以在login.html
中可以查看到,js就是对这些元素进行了选择var qzLogin = $(".qz-login");
var noticeBox = $(".notice-box");
login.js
中定义了一个html中样式为qz-login
的按钮按下的事件,查看login.html
可以得知该按钮就为登录按钮,按下后就进入log()函数进行登录验证qzLogin.click(function () {
log();
});
pass
的按键松开情况,同理查看html
文件可知实际就为密码输入框,弹起后检查是否为回车键,是的话进入log()
函数进行登录验证$(".pass").keyup(function (event) {
if (event.which == "13") {
log();
}
});
该函数首先过滤低级错误,比如电话不符合规范,密码长度不符合等
var phone = $("#username").val().trim();
var pass = $("#password").val();
var myreg = /^(((12[0-9]{1})|(13[0-9]{1})|(14[0-9]{1})|(17[0]{1})|(15[0-3]{1})|(15[5-9]{1})|(18[0-9]{1}))+\d{8})$/;
if(phone.length == 0 || phone.length != 11 || !myreg.test(phone) ){
$(".notice-box-res").show();
} else if(pass.length < 6 || pass.length > 18){
$(".notice-box-password-num").show();
}
当基本条件符合后就进行请求,请求loginUser
链接
var str = {phone: phone, password: pass};
$.ajax({
type: "GET",
url: "loginUser",
// contentType: "application/x-www-form-urlencoded",
contentType: "application/json",
dataType: "json",
data: str,
该链接的处理在LoginController
中有定义,Controller
通过调用Shiro
中的SecurityUtils
进行用户名和密码认证,成功返回200状态码,失败返回500状态码,并传回msg
信息
@GetMapping("loginUser")
public JsonResult loginUser(@RequestParam("phone") String phone,
@RequestParam("password") String password,
HttpSession session){
UsernamePasswordToken token = new UsernamePasswordToken(phone, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
UserVOEntity user = (UserVOEntity) subject.getPrincipal();
user.setPassword("");
session.setAttribute("user", user);
return new JsonResult(200, "ok", null);
} catch (AuthenticationException e){
return new JsonResult(500, "用户名或密码错误", null);
}
}
log函数成功获取到请求返回的结果后,密码错误则提示,正确则跳转至"/lasturl"页面
success: function (data) {
//放入数据
if(data.status == 200){
window.location.href="/lasturl";
}else if(data.status == 500){
$(".notice-box-res").show();
setTimeout(function () {
noticeBox.hide();
}, 3000);
}
},