1.JPA注解给添加唯一约束
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"role_name"}))
然后测试发现并没有生效,发现相同的名称可以多次插入,把控制台打印的sql语句直接拿去数据库运行报错了
ERROR 1071 (42000): Specified key was too long; max key length is 1000 bytes
意思是说字段长度太长,jpa在定义字段的时候如果没有设置默认类型和长度的话会自动默认为 varchar(250),也就是1000 bytes,而设置唯一键不能超过这个长度,所以要设置长度
@Column( name = "role_name", nullable = false, length = 20)
或者
@Column(name = "role_name",columnDefinition = "varchar(20) default '12345' ")
2.实体类序列化
在后续登录测试中,user对象需要存储在redis缓存里
要求user对象必须可序列化,否则会报错。实现Serializable即可
public class User implements Serializable
为什么要序列化
背景
1.java对象不能跨进程传递
2.byte数组可被各种stream处理所以,想传输java对象,就要把对象转换为byte数组。
概念
序列化:按照编码协议把java对象转化为byte数组。
反序列化:按照编码协议把byte数组转化为java对象。
编码协议:json 表单 protobuf thrift 等等,哪怕是你自定义的协议,能够编码解码就行。
传输协议:http ftp thrift grpc dubbo等,以及自定义。
网络五层:
应用层:就是传输协议加上编码协议,比如http+json
传输层:tcp udp 负责解析端口
网络层:负责解析ip地址
链路层 arp pppoe之类的 比如拨号上网物理层 光纤 卫星等传输01作者:奋斗无底线 链接:https://www.zhihu.com/question/67037207/answer/358740300
来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在hibernate里,并非所有的实体类必须实现序列化接口,因为在hibernate中我们通常是将基本类型的数值映射为数据库中的字段。而基础类型都实现了序列化接口(String也实现了)。 所以,只有在想将一个对象完整存进数据库(存储为二进制码),而不是将对象的属性分别存进数据库,读取时再重新构建的话,就可以不用实现序列化接口。凡是可以序列化的对象都可以持久化,极端的说,我们可以只建立一个表Object(OID,Bytes),但基本上没有人这么做,因为一旦这样,我们就失去了关系数据库额外的统计分析功能。
serialVersionUID
serialVersionUID的作用就是保证对象一致性,虚拟机反序列化时会验证serialVersionUID。如果不写虚拟机会默认一个值,这样在不同服务器,不同平台上对象的serialVersionUID可能会不同,导致反序列失败。详情见下面链接。
讲真,下次打死我也不敢随便改serialVersionUID了
总的实体类代码
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"role_name"}))
public class Role implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
//角色名称
@Column( name = "role_name", nullable = false, length = 20)
private String roleName;
@ManyToMany(mappedBy = "roleSet")
private Set<User> userSet;
@ManyToMany(cascade={CascadeType.PERSIST,CascadeType.MERGE})
@JoinTable(name = "role_permission", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "permission_id", referencedColumnName = "id"))
private Set<Permission> permissionSet;
}
shiro-redis包部署时报错
protected method redis.clients.jedis.JedisPool.returnResource
at org.crazycake.shiro.RedisManager.get(RedisManager.java:56)
自Jedis3.0版本后jedisPool.returnResource()遭弃用,官方重写了Jedis的close方法用以代替;
要么Jedis回退到2.9.0版本,要么将shiro-redis更新到新版本
shiro前后端分离登录测试
前后端不分离的项目,一般登录验证信息是通过表单传输,然后直接进入shiro验证后返回“index.html”。但是前后端分离项目则是通过json传输,不能进入shiro默认校验流程,因此首先在过滤器中将“/login”,设为不拦截。
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器.
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
//配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
//<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/sign", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/**", "authc");
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
// 配器shirot认登录累面地址,前后端分离中登录累面跳转应由前端路由控制,后台仅返回json数据, 对应LoginController中unauth请求
//shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
// shiroFilterFactoryBean.setSuccessUrl("/home");
//未授权界面;
// shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
然后再controller中调用
@PostMapping("/login")
public String login(@RequestBody User user){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword());
try {
subject.login(token);
}catch (Exception e){
return "登录失败!";
}
return "登录成功!";
}
SecurityUtils.getSubject()与当前线程绑定,从中可以获取登录信息,subject.login(token);可以调用Shiro的验证流程,具体验证规则在我们自定义的shiroRealm中。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
// //获取用户的输入的账号.
String username = (String) token.getPrincipal();
System.out.println(token.getCredentials());
//通过username从数据库中查找 User对象,如果找到,没找到.
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
User user = userService.getUserByName(username);
// System.out.println("----->>userInfo="+user.getUserName());
if(user == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //user对象
user.getPassword(), //数据库密码
ByteSource.Util.bytes(user.getSalt()),//salt
getName() //realm name
);
return authenticationInfo;
}
至于要不要将user信息置入缓存,我认为没什么必要,除非遭受攻击,否则不会太频繁调用用户数据(真的不是因为懒)
具体调用过程见下图,一直到校验密码那步
为什么只到密码验证这步?因为对它爱得深沉。。。
在密码加密器这边用的网上抄的MD5算法,散列两次,然后注释写着md5(md5("")),旧版本也许可以,但是新版本这样不行
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
然后注册成功后发现一直登录不上,报凭证错误也就是密码错了,然后看shiro源码发现其生成密码大致如下(盐暂时弄为空)
SimpleHash simpleHash = new SimpleHash(algorithmName, user.getPassword(), null,2);
其内部
protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws UnknownAlgorithmException {
MessageDigest digest = this.getDigest(this.getAlgorithmName());
if (salt != null) {
digest.reset();
digest.update(salt);
}
byte[] hashed = digest.digest(bytes);
int iterations = hashIterations - 1;
for(int i = 0; i < iterations; ++i) {
digest.reset();
hashed = digest.digest(hashed);
}
return hashed;
}
也就是说md5(md5())并不能等价于
String newPassword1 = new SimpleHash(algorithmName, user.getPassword(), null,2).toHex();
而是等价于
String newPassword2 = new SimpleHash(algorithmName, new SimpleHash(algorithmName, user.getPassword(), null, 1).toHex(), null, 1).toHex();
即
String newPassword2 = new SimpleHash(algorithmName, newPassword1, null, 1).toHex();
md5(md5())过程:
1.密码转为byte[],数组长度为密码长度
2.byte[]经digest即取摘要加密变成byte[16]
3.byte[16]转为byte[32]
4.byte[32]转为长度为32位字符串,如8b810750f6a3cea321b2146bf3367eca,第一次加密结束
5.32位字符串转为byte[32]
6.byte[32]经digest即取摘要加密变成byte[16]
7.byte[16]转为byte[32]
8.byte[32]转为长度为32位字符串,第二次加密结束
Shiro MD5加密过程,即new SimpleHash(algorithmName, user.getPassword(), null,2).toHex():
1.密码转为byte[],数组长度为密码长度
2.byte[]经digest加密变成byte[16]
6.byte[16]再次digest加密变成byte[16]
7.byte[16]转为byte[32]
8.byte[32]转为长度为32位字符串,第二次加密结束
区别在于md5(md5())是一次加密后将上一次加密的结果字符串当作参数重新在加密一次,而SimpleHash内部则是在加密的过程中直接再次加密,最后生成字符串。从安全角度考虑后者更安全些,不容易被解密网站破解。因此,在注册时密码加密不能使用md5(md5())加密。
md5:byte[16]转为byte[32]
public static String encode(byte[] bytes, boolean upperCase) {
if (bytes == null) {
return null;
} else {
char[] chars = upperCase ? UPPER_CHARS : LOWER_CHARS;
char[] hex = new char[bytes.length * 2];
for(int i = 0; i < bytes.length; ++i) {
int b = bytes[i] & 255;
hex[i * 2] = chars[b >> 4];
hex[i * 2 + 1] = chars[b & 15];
}
return new String(hex);
}
}
注册加密(加盐)
@RestController
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/sign")
public String signUser(@RequestBody User user){
if(null != userService.getUserByName(user.getUserName())){
return "该用户名已注册!";
} else {
// new SimpleHash(algorithmName, user.getPassword(), ByteSource.Util.bytes(user.getUsername()), hashIterations).toHex();
user.setSalt(user.getUserName());
String password = "";
user.setPassword(new SimpleHash("md5", user.getPassword(), ByteSource.Util.bytes(user.getUserName()), 2).toHex());
if (null != userService.addUser(user)){
return "注册成功!";
} else {
return "注册失败!";
}
}
}
@PostMapping("/login")
public String login(@RequestBody User user){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword());
try {
subject.login(token);
}catch (Exception e){
return "登录失败!";
}
return "登录成功!";
}
public UserService getUserService() {
return userService;
}
public void setUserService(UserService userService) {
this.userService = userService;
}
}