开发踩坑日志3 jpa表字段约束,shiro前后端分离中的注册,登录密码加密及缓存相关

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缓存里
开发踩坑日志3 jpa表字段约束,shiro前后端分离中的注册,登录密码加密及缓存相关_第1张图片
要求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");

        //:这是一个坑呢,一不小心代码就不好使了;
        //
        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信息置入缓存,我认为没什么必要,除非遭受攻击,否则不会太频繁调用用户数据(真的不是因为懒)
具体调用过程见下图,一直到校验密码那步开发踩坑日志3 jpa表字段约束,shiro前后端分离中的注册,登录密码加密及缓存相关_第2张图片
为什么只到密码验证这步?因为对它爱得深沉。。。
在密码加密器这边用的网上抄的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;
    }
}

你可能感兴趣的:(开发踩坑日志3 jpa表字段约束,shiro前后端分离中的注册,登录密码加密及缓存相关)