Spring+Shiro权限管理 (一) 使用MD5+salt(盐)加密、认证

以下是基于spring和Shiro的整合,此篇要点分为两部分:新增用户时,使用MD5和盐加密用户密码;使用shiro认证用户两部分。由于该小项目是完成后总结的,步骤和正常开发可能有些出入,还有该项目异常部分应用了日志记录,具体的日志配置可参考我上篇文章。

 

思路:

账户、密码认证:

1、创建Subject主体;

2、将从前端得到的账号,密码存放到Token中;

3、再使用subject.login(token)提交认证;

4、UserRealm的doGetAuthenticationInfo认证方法中,通过从token中拿到账户号,从数据查询出用户的密码和盐;new 一个SimpleAuthenticationInfo ,将账户名、密码、盐(使用ByteSource.Util.bytes()方法转换为ByteSource类型)、当前Realm的name封装进去。

4、返回SimpleAuthenticationInfo对象(该对象会传入凭证匹配器中),凭证匹配器HashedCredentialsMatcher 会根据Token(你前台登录时的明文账号密码,和数据库查询出来的加密后的密码比较)

 

实现步骤:

 

(一)准备工作(基于SSM框架创建项目):

引入maven依赖:

    
        UTF-8
        1.7
        1.7
        5.1.8.RELEASE
        2.9.8
    

    
        
            junit
            junit
            4.12
            test
        

        
            org.springframework
            spring-beans
            ${spring.version}
        
        
            org.springframework
            spring-core
            ${spring.version}
        
        
            org.springframework
            spring-context
            ${spring.version}
        
        
            org.springframework
            spring-web
            ${spring.version}
        
        
            org.springframework
            spring-webmvc
            ${spring.version}
        
        
            org.springframework
            spring-jdbc
            ${spring.version}
        
        
            org.springframework
            spring-test
            ${spring.version}
        

        
            org.mybatis
            mybatis
            3.5.1
        

        
            org.mybatis
            mybatis-spring
            2.0.1
        
        
        
            mysql
            mysql-connector-java
            8.0.16
        
        
        
            com.alibaba
            druid
            1.1.10
        

        
            jstl
            jstl
            1.2
        

        
            javax.servlet
            javax.servlet-api
            3.1.0
            provided
        
        
        
            javax.servlet.jsp
            javax.servlet.jsp-api
            2.3.1
            provided
        

        
        
            org.apache.shiro
            shiro-core
            1.4.0
        
        
            org.apache.shiro
            shiro-spring
            1.4.0
        

    

login.jsp(登录页面)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

    
        登录界面
    
    
        
账号:${userName}
密码:${password}

home.jsp(主页面)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

    
        主页面
    
    
        

登录成功!

addUser.jsp(新增用户界面)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>


    新增用户


    
姓名:
密码:
年龄:
性别:

showIdCode.jsp(新增成功后的反馈界面)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>


    当前用户的员工编号

    
        

新增员工成功!新增员工编号为${IdCode}!


接着是数据库表结构:

Spring+Shiro权限管理 (一) 使用MD5+salt(盐)加密、认证_第1张图片

实体类User

package com.xcj.jquery_ajax.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class User {

    //用户编号(账号,自增)
    private Integer idCode;
    private String password;
    private String name;
    private int age;
    private String sex;
    //盐
    private String salt;
    
}

Dao层(UserDao):

package com.xcj.jquery_ajax.dao;

import com.xcj.jquery_ajax.entity.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository("UserDao")
public interface UserDao {
    public User findUserByIdCode(@Param("idCode") Integer id_code);
    public void addUser(User user);
}

UserDao.xml






    
        
        
        
        
        
        
    
    
    

    
        INSERT INTO user (password,name,age,sex,salt) VALUES (#{password},#{name},#{age},#{sex},#{salt})
    


UserService

package com.xcj.jquery_ajax.service;

import com.xcj.jquery_ajax.entity.User;

import java.util.List;

public interface UserService {
    
    /*根据员工编号查询用户数据*/
    public User findUserByIdCode(Integer idCode);
    /*新增员工*/
    public void addUser(User user);
}

UserServiceImp

package com.xcj.jquery_ajax.service.serviceImp;

import com.xcj.jquery_ajax.dao.UserDao;
import com.xcj.jquery_ajax.entity.User;
import com.xcj.jquery_ajax.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("UserService")
public class UserServiceImp implements UserService {

    @Autowired
    UserDao userDao;

    @Override
    public User findUserByIdCode(Integer idCode) {
        return userDao.findUserByIdCode(idCode);
    }

    @Override
    public void addUser(User user) {
        userDao.addUser(user);
    }
}

UserController

新建用户步骤:

1、新建User对象,设值(自定义随机数生成工具类MyRandomUtil,生成盐;使用SimpleHash类对密码进行加密)

2、调用UserService类新增用户方法。

shiro认证的步骤:

1、创建Subject主体;

2、将从前端得到的账号,密码存放到Token中;

3、再使用subject.login(token)提交认证,可能会发生异常,这里我只处理了两个异常。

package com.xcj.jquery_ajax.controller;

import com.xcj.jquery_ajax.entity.User;
import com.xcj.jquery_ajax.service.UserService;
import com.xcj.jquery_ajax.tool.MyRandomUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller("UserController")
public class UserController {

    @Autowired
    UserService userService;

    //跳转登录界面
    @RequestMapping("login")
    public ModelAndView login() {
        return new ModelAndView("login");
    }

    //shiro登录认证流程
    @RequestMapping("toHome")
    public ModelAndView toHome(String userIdCode , String password){
        ModelAndView modelAndView = new ModelAndView();
        
        //通过shiro的一个工具类SecurityUtil获取主体subject
        Subject subject = SecurityUtils.getSubject();

        //UsernamePasswordToken用于实现基于用户名/密码主体(Subject)身份认证。
        //UsernamePasswordToken实现了 RememberMeAuthenticationToken 和HostAuthenticationToken,可以实现“记住我”及“主机验证”的支持。
        UsernamePasswordToken token = new UsernamePasswordToken(userIdCode, password);

        try {
            //当调用subject的登入方法时,会跳转到认证的方法上。
            subject.login(token);

        } catch (UnknownAccountException e) {
            e.printStackTrace();
            modelAndView.addObject("userName", "用户id错误!");
            modelAndView.setViewName("login");
            return modelAndView;
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            modelAndView.addObject("password", "用户密码错误!");
            modelAndView.setViewName("login");
            return modelAndView;
        }
        modelAndView.setViewName("home");
        return modelAndView;
    }

    //跳转到新增用户界面
    @RequestMapping("toAddUser")
    public ModelAndView toAddUser(){
        return new ModelAndView("addUser");
    }

    //新增用户处理
    @RequestMapping(value = "addUser",method = RequestMethod.POST)
    public ModelAndView addUser(String name,Integer age,String sex,String password){

        User user = new User();
        user.setName(name);
        user.setAge(age);
        user.setSex(sex);
        //自定义随机数生成工具类,生成盐
        String salt = MyRandomUtil.getRandom();
        user.setSalt(salt);
        //使用SimpleHash类对密码进行加密
        String pwd = new SimpleHash("MD5",password, ByteSource.Util.bytes(salt),1).toHex();
        user.setPassword(pwd);

        userService.addUser(user);

        //idCode的值由MyBatis的useGeneratedKeys属性返回
        Integer idCode = user.getIdCode();

        return new ModelAndView("showIdCode").addObject("IdCode",idCode);   
    }

}

随机数生成工具类MyRandomUtil 

package com.xcj.jquery_ajax.tool;

import java.util.Random;

public class MyRandomUtil {

    public static String getRandom() {
        String str = "";
        char[] ch = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
                'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
                'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
        Random random = new Random();
        for (int i = 0; i < 8; i++) {
            char num = ch[random.nextInt(ch.length)];
            str += num;
        }
        return str;
    }

}

UserRealm(自定义Realm),暂时只涉及认证,授权过程将在后续更新。

认证过程:

1、从token中拿到username,即用户账号(这是我们在controller层传进去的 idCode)。

2、通过username从数据库获取User对象,获取对应密码(数据库存放的密码已经过加密处理)和salt(盐)

3、new 一个SimpleAuthenticationInfo ,将账户名、密码、盐(使用ByteSource.Util.bytes()方法转换为ByteSource类型)、当前Realm的name封装进去。

4、返回SimpleAuthenticationInfo对象(该对象会传入凭证匹配器中)。

package com.xcj.jquery_ajax.realm;

import com.xcj.jquery_ajax.entity.User;
import com.xcj.jquery_ajax.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
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;

public class UserRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    /*授权*/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /*认证*/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        String username = token.getUsername();

        User user = null;
        if (username != null && !username.equals("")) {
            //通过token中获取到的账号(username)从数据库中拿到user对象
            user = userService.findUserByIdCode(Integer.valueOf(username));
        }

        if (user != null) {
            String password = user.getPassword();
            String salt = user.getSalt();
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes(salt), this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}

applicationContext.xml中,我们将UserRealm的凭证匹配器改为了HashedCredentialsMatcher。它会解析SimpleAuthenticationInfo,帮我们比较账户名、密码是否一致,如果不一致将返回对应Exception。




    
    
    
    

    
    
        
            classpath:applicationContext.properties
        
    

    
    
        
        
        
        

        
        
        

        
        

        
        
        
        

        
        
        

    

    
    
        
        
    

    
    
        
        
    

    
    
        
        
        
        
    

    
    

    
    
    

    
    
        

        
        
        
        
        


        
        
            
                
                
            
        
        
        
            
                /login = anon 
                /toHome = anon
                /logout = logout
                /** = authc
            
        

    

    
    
        
    

    
    
        
            
                
            
        
    

    
    
        
    

    
    
        
        
    

    
    
        
    

 applicationContext.properties

#############################
## 数据库源 ##
#spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jquery_ajax?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT
spring.datasource.username=root
spring.datasource.password=123456

# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大(连接数 = ((核心数 * 2) + 有效磁盘数))
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=10

# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000

# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
#用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。
spring.datasource.validationQuery=SELECT 1 FROM DUAL
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
spring.datasource.testWhileIdle=true
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
spring.datasource.testOnBorrow=false
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
spring.datasource.testOnReturn=false

# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.filters=stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
spring.datasource.useGlobalDataSourceStat=true

web.xml

applicationContext.xml中shiro的拦截器bean的name属性必须与web.xml的filter的name属性相同(配置了targetBeanName属性,则以targetBeanName为主)。




    jquery_ajax


    
    
        springmvc
        org.springframework.web.servlet.DispatcherServlet
        
        
            contextConfigLocation
            classpath:applicationContext.xml
        
        1
    

    
        springmvc
        
        /
    

    
    
        characterEncodingFilter
        org.springframework.web.filter.CharacterEncodingFilter
        
            encoding
            UTF-8
        
        
            forceEncoding
            true
        
    
    
        characterEncodingFilter
        /*
    

    !--配置shiro过滤器,DelegatingFilterProxy通过代理模式将spring容器中的bean和filter关联起来-->
    
        shiroFilter
        org.springframework.web.filter.DelegatingFilterProxy
        
        
            targetFilterLifecycle
            true
        
        
        
            targetBeanName
            shiroFilter
        
    
    
        shiroFilter
        /*
    
    

    
    
        druidWebStatFilter
        com.alibaba.druid.support.http.WebStatFilter
        
            exclusions
            /assets/*,*.css,*.js,*.gif,*.jpg,*.png,*.ico,*.eot,*.svg,*.ttf,*.woff,*.jsp,*.tpl,/druid/*
            
        
    
    
        druidWebStatFilter
        /*
    

    
        druidStatView
        com.alibaba.druid.support.http.StatViewServlet
        
            
            loginUsername
            admin
        
        
            
            loginPassword
            123456
        
    
    
        druidStatView
        /druid/*
    


因为我们在applicationContext.xml中设置的统一异常页面处理:

所以,我们来实现下MyExceptionResolver

package com.xcj.jquery_ajax.execption;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class MyExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        //e.printStackTrace();
        log.error("异常原因:{}  ;  异常信息:{}",e.getCause(),e.getMessage());

        ModelAndView mv = new ModelAndView("error");
        mv.addObject("exception", e.toString().replaceAll("\n", "
")); return mv; } }

error.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>


    异常

请联系管理员


${exception}

以上,认证过程就搞定了。

(二)、演示效果

Spring+Shiro权限管理 (一) 使用MD5+salt(盐)加密、认证_第2张图片

Spring+Shiro权限管理 (一) 使用MD5+salt(盐)加密、认证_第3张图片

Spring+Shiro权限管理 (一) 使用MD5+salt(盐)加密、认证_第4张图片

Spring+Shiro权限管理 (一) 使用MD5+salt(盐)加密、认证_第5张图片

总结,此篇中的Shiro认证过程不算复杂,我在代码中做了详细的注释,应该也不难看懂。

如果你是刚学Shiro认证的话,不凡模仿下我这个例子。

若发现我有错误的话,欢迎指正,我们互相学习,拜拜!

你可能感兴趣的:(Shiro)