以下是基于spring和Shiro的整合,此篇要点分为两部分:新增用户时,使用MD5和盐加密用户密码;使用shiro认证用户两部分。由于该小项目是完成后总结的,步骤和正常开发可能有些出入,还有该项目异常部分应用了日志记录,具体的日志配置可参考我上篇文章。
(一)准备工作(基于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" %>
登录界面
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}!
接着是数据库表结构:
实体类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}