Springboot前后端分离-整合Shiro-Md5加密与认证登录——学习记录
参考借鉴这里:https://blog.csdn.net/bbxylqf126com/article/details/110501155
https://blog.csdn.net/weixin_42375707/article/details/111145907
https://blog.csdn.net/qq_34845394/article/details/94858168
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- SpringBootText注解依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Junit依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
<!--日志-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-log4j12</artifactId>-->
<!-- <version>1.7.21</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>jcl-over-slf4j</artifactId>-->
<!-- <version>1.7.21</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>commons-logging</groupId>-->
<!-- <artifactId>commons-logging</artifactId>-->
<!-- <version>1.1.3</version>-->
<!-- </dependency>-->
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
直接上代码(我认为关键)
shiro核心配置
package com.ztxue.mybatis_plus.shiro;
import com.ztxue.mybatis_plus.utils.ShiroConstant;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
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;
/**
* Shiro的核心配置类,用来整合shiro框架
*/
@Configuration
public class ShiroConfiguration {
//1.引入自定义realm-CustomerRealm()
@Bean
public Realm getRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
// 设置密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密方式
credentialsMatcher.setHashAlgorithmName(ShiroConstant.HASH_ALGORITHM_NAME.MD5);
// 设置散列次数
credentialsMatcher.setHashIterations(ShiroConstant.HASH_ITERATORS);
shiroRealm.setCredentialsMatcher(credentialsMatcher);
return shiroRealm;
}
//2.创建安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//关联realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//3.创建过滤工厂-负责拦截所有请求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//过滤器链映射
Map<String, String> filter = new LinkedHashMap<>();
/*
* 常用过滤器如下
* anon:无需认证访问
* authc:必须认证了才能访问
* user:记住我开启才可以访问
* perms:拥有对某个资源的权限才能访问
* */
filter.put("/user/login","anon");
filter.put("/user/regis","anon");
// 配置不会被拦截的链接 顺序判断,必须配置到每个静态目录
filter.put("/swagger-ui.html/**", "anon");
filter.put("/webjars/**","anon");
// 所有url拦截
// filter.put("/**", "authc");
// 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了, 位置放在 anon、authc下面
filter.put("/user/logout", "logout");
// 修改shiro默认登录地址,登录成功之后返回用户基本信息及token给前端
bean.setLoginUrl("/user/login");
// 设置成功之后要跳转的链接
bean.setSuccessUrl("/user/regis");
// 拦截未授权路径
bean.setUnauthorizedUrl("/user/unauthorized");
//过滤器链传值
bean.setFilterChainDefinitionMap(filter);
return bean;
}
}
自定义的realm
package com.ztxue.mybatis_plus.shiro;
import com.ztxue.mybatis_plus.fv_sys.service.user.FvUserService;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* 自定义realm--放弃使用.ini文件,使用数据库查询
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
FvUserService userService;
@Autowired
FvRoleService roleService;
@Autowired
FvPermissionService permissionService;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的输入的账号
String userName = (String) token.getPrincipal();
System.out.println("userName----------------------------->>>" + userName);
//通过username从数据库中查找 User对象.
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
FvUser user = userService.findByName(userName);
System.out.println("user.getUPassword()----------------------------->>>" + user.getUPassword());
System.out.println("userName----------------------------->>>" + userName);
if (!ObjectUtils.isEmpty(user)) {
return new SimpleAuthenticationInfo(
// 也可以写用户名
user,
// 传入的是从数据库中获取到的password,然后再与token中的password进行对比匹配
user.getUPassword(),
// salt–用于加密密码对比。若不需要,则可以设置为空 “ ”
ByteSource.Util.bytes(user.getUSalt()),
// 当前realm的名字
getName()
);
}
return null;
}
}
注意这个地方
new SimpleAuthenticationInfo(
// 也可以写用户名
user,
// 传入的是从数据库中获取到的password,然后再与token中的password进行对比匹配
user.getUPassword(),
// salt–用于加密密码对比。若不需要,则可以设置为空 “ ”
ByteSource.Util.bytes(user.getUSalt()),
// 当前realm的名字
getName()
)
SimpleAuthenticationInfo里面三个或四个参数,第三个–ByteSource.Util.bytes(user.getUSalt()),
获取的盐值
随机盐生成工具类(可收藏学习)
package com.ztxue.mybatis_plus.utils;
import java.util.Random;
/**
* 用户随机盐生成工具类
*/
public class SaltUtil {
/**
* 生成salt的静态方法
* @param n
* @return
*/
public static String getSalt(int n){
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
char aChar = chars[new Random().nextInt(chars.length)];
sb.append(aChar);
}
return sb.toString();
}
}
MD5加密说明类(定义成常量而已,我感觉duck不必,可能是我水平不够)
package com.ztxue.mybatis_plus.utils;
public class ShiroConstant {
/** 随机盐的位数 **/
public static final int SALT_LENGTH = 8;
/** hash的散列次数 **/
public static final int HASH_ITERATORS = 1024;
/** 加密方式 **/
public interface HASH_ALGORITHM_NAME {
String MD5 = "MD5";
}
}
接下来是常规
controller关键代码
package com.ztxue.mybatis_plus.fv_sys.controller.user;
import com.ztxue.mybatis_plus.config.exception.LoginException;
import com.ztxue.mybatis_plus.fv_sys.entity.user.FvUser;
import com.ztxue.mybatis_plus.fv_sys.mapper.user.UserMapper;
import com.ztxue.mybatis_plus.fv_sys.service.user.FvUserService;
import com.ztxue.mybatis_plus.result.AjaxResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author 张童学
* @since 2021-07-17
*/
@RestController
@RequestMapping("/user")
@Api(description = "用户页")
public class FvUserController {
@Autowired
FvUserService fvUserService;
@Autowired
UserMapper userMapper;
@ApiOperation("注册")
@PostMapping("/regis")
public AjaxResult register(FvUser user) {
try {
fvUserService.register(user);
return AjaxResult.success("注册成功!",user);
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error("注册失败!大侠请从头再来!");
}
}
@ApiOperation("登录")
@RequestMapping("/login")
public AjaxResult login(String userName, String password) {
// 获取Subject实例对象,用户实例
Subject currentUser = SecurityUtils.getSubject();
// 将用户名和密码封装到UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
System.out.println("token============>" + token);
try {
// 传到 MyShiroRealm 类中的方法进行认证
currentUser.login(token);
return AjaxResult.success(token);
} catch (UnknownAccountException e) {
throw new LoginException("账号不存在!", e);
} catch (IncorrectCredentialsException e) {
throw new LoginException("密码不正确!", e);
} catch (AuthenticationException e) {
throw new LoginException("用户验证失败!", e);
}
// 登录成功返回用户信息
}
}
这里用上了封装的结果集
package com.ztxue.mybatis_plus.result;
import com.ztxue.mybatis_plus.utils.HttpStatus;
import com.ztxue.mybatis_plus.utils.StringUtils;
import java.util.HashMap;
/**
* 操作消息提醒
* */
public class AjaxResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
public static final String CODE_TAG = "code";
/**
* 返回内容
*/
public static final String MSG_TAG = "msg";
/**
* 数据对象
*/
public static final String DATA_TAG = "data";
/**
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
*/
public AjaxResult() {
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
*/
public AjaxResult(int code, String msg) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(int code, String msg, Object data) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (StringUtils.isNotNull(data)) {
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success() {
return AjaxResult.success("操作成功");
}
/**
* 返回成功数据
*
* @return 成功消息
*/
public static AjaxResult success(Object data) {
return AjaxResult.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static AjaxResult success(String msg) {
return AjaxResult.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static AjaxResult success(String msg, Object data) {
return new AjaxResult(HttpStatus.SUCCESS, msg, data);
}
/**
* 返回错误消息
*
* @return
*/
public static AjaxResult error() {
return AjaxResult.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(String msg) {
return AjaxResult.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult error(String msg, Object data) {
return new AjaxResult(HttpStatus.ERROR, msg, data);
}
/**
* 返回错误消息
*
* @param code 状态码
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(int code, String msg) {
return new AjaxResult(code, msg, null);
}
}
serviceImpl 部分
@Autowired
UserMapper userMapper;
@Override
public FvUser findByName(String name) {
return userMapper.findByName(name);
}
// 注册
@Override
public void register(FvUser user) {
// 生成随机盐
String salt = SaltUtil.getSalt(ShiroConstant.SALT_LENGTH);
// 保存随机盐
user.setUSalt(salt);
// 生成密码
Md5Hash password = new Md5Hash(user.getUPassword(), salt, ShiroConstant.HASH_ITERATORS);
// 保存密码
user.setUPassword(password.toHex());
userMapper.insert(user);
System.out.println("生成的盐------------------>"+salt);
System.out.println("MD5加密的密码------------------>"+password);
}
serivce 部分
FvUser findByName(String name);
// 注册
void register(FvUser user);
mapper
package com.ztxue.mybatis_plus.fv_sys.mapper.user;
import com.ztxue.mybatis_plus.fv_sys.entity.user.FvUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 张童学
* @since 2021-07-17
*/
@Mapper
public interface UserMapper extends BaseMapper<FvUser> {
@Select("select * from fv_user where u_name = #{uName}")
FvUser findByName(String name);
@Insert("INSERT INTO fv_user ( u_name, u_password, is_deleted, gmt_create, gmt_modified, u_salt ) VALUES (#{uName},#{uPassword},#{isDeleted},#{gmtCreate},#{gmtModified},#{uSalt})")
int add(FvUser user);
}
其实mapper 我用了mybatis_plus ,继承了它的基本mapper,很多方法可以不用写,但我还不太熟。
然后这是我的实体类和数据库
package com.ztxue.mybatis_plus.fv_sys.entity.user;
import com.baomidou.mybatisplus.annotation.*;
import java.util.Date;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
*
* </p>
*
* @author 张童学
* @since 2021-07-17
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class FvUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
@TableId(value = "u_id", type = IdType.AUTO)
private Integer uId;
/**
* 用户名
*/
private String uName;
/**
* 用户手机
*/
private String uPhone;
/**
* 邮箱
*/
public String uEmail;
/**
* 密码
*/
public String uPassword;
/**
* 逻辑删除
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer isDeleted;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
/**
* 密码盐. 重新对盐重新进行了定义,用户名+salt,这样就不容易被破解,可以采用多种方式定义加盐
*/
@TableField(value = "u_salt")
private String uSalt;
}
CREATE TABLE `fv_user` (
`u_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户id',
`u_name` varchar(255) DEFAULT NULL COMMENT '用户名',
`u_phone` varchar(255) DEFAULT NULL COMMENT '用户手机',
`u_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '邮箱',
`u_password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
`u_salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '盐',
`u_state` int NOT NULL DEFAULT '1' COMMENT '状态',
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`u_id`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
忽略部分前缀和部分无用字段
写在后面
博主目前在学shiro,因为在暑假实习,然后实习企业用的是shiro不是springSecurity,而且企业前后端分离,我刚好对前后端分离不熟悉,就趁着一块学了,但是碰上许多困难。
前后端分离需要写API,测试API我用swagger(现学现用就很nice)框架和APIPost这个软件(Postman我实在用不惯)。
shiro目前学基本的认证+MD5加密就搞得我头昏脑涨,因为博主本人“大聪明”,还总是好高骛远。
MD5加密逻辑不复杂,就“用户注册”时加密存入数据库,“用户认证”时从数据库取出来的加密密码,shiro能解析,不就酱紫?
后面也会有shiro的学习记录,这是vx:handsomeztx。希望有小伙伴一块学习交流、大佬批评指教。