项目github地址:https://github.com/pc859107393/SpringMvcMybatis
实时项目同步的地址是国内的码云:https://git.oschina.net/859107393/mmBlog-ser
我的首页是:http://www.jianshu.com/users/86b79c50cfb3/latest_articles
上一期是:[手把手教程][第二季]java 后端博客系统文章系统——No7
时隔这么久,再次开始更新这个系列的项目,这里向大家说声对不起!理想是需要面包支撑才能走下去!
工具
- IDE为idea2017.1.5
- JDK环境为1.8
- gradle构建,版本:2.14.1
- Mysql版本为5.5.27
- Tomcat版本为7.0.52
- 流程图绘制(xmind)
- 建模分析软件PowerDesigner16.5
- 数据库工具MySQLWorkBench,版本:6.3.7build
本期目标
完成Shiro认证管理和权限管理
认证模块
谈到认证,我们就得明白什么是认证。传统的来说,就是用户登录后获取到用户信息,然后把用户信息进行比对,比对结果相同则通过验证反之则是验证失败。
传统的用户认证方式是什么样的呢?
a、用户登录(userName和userPassword) -> b、从数据库获取用户信息(userBean) -> c、把userPassword加密和userBean的密码对比 -> d、对比相同登陆成功,反之登录失败
同样的在这个流程中产生任何其他异常,均是登录失败!
那么Shiro认证又是怎么回事呢?
a、获得Subject -> b、构造UsernamePasswordToken -> c、调用Subject.login() -> d、实现AuthorizingRealm并执行认证方法 -> e、实现SimpleCredentialsMatcher并实例化该对象且执行doCredentialsMatch()方法进行密码比对
既然上面我们完成了思路流程整理,那么下面我们直接完成Shiro的登录功能!
@Controller
@Api(description = "用户相关")
public class SysUserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/user/login", method = RequestMethod.POST, produces = APPLICATION_XHTML_XML_VALUE)
@ApiOperation(value = "/user/login", notes = "登录系统web接口")
public String login(HttpServletRequest request, HttpServletResponse response, ModelMap map,
@ApiParam(value = "用户名不能为空,否则不允许登录"
, required = true) @RequestParam(value = "userLogin", required = false) String userLogin,
@ApiParam(value = "用户密码不能为空且必须为16位小写MD5,否则不允许登录"
, required = true) @RequestParam(value = "userPass", required = false) String userPass) {
User user = null;
try {
//1.得到Subject
Subject subject = SecurityUtils.getSubject();
//2.调用登录方法
UsernamePasswordToken token = new UsernamePasswordToken(userLogin, userPass);
subject.login(token);//当这一代码执行时,就会自动跳入到AuthRealm中认证方法
user = (User) subject.getPrincipal();
} catch (Exception e) {
e.printStackTrace();
map.addAttribute("msg", e.getMessage());
return "userLogin";
}
userService.updateUserSession(userLogin, request.getRequestedSessionId());
//此处查找出来的数据已经缓存在内存中,所以这里需要将内存中user的sessionId改变后再写到session中
user.setUserSessionId(request.getRequestedSessionId());
request.getSession().setAttribute("userInfo", user);
return "redirect:/endSupport/index";
}
}
上面的代码简单的来说就是构建了一个仅仅支持POST方法的连接,连接地址是:“域名:端口号/user/login”,登陆成功就返回后端主页,登录失败返回包含错误信息的登录页面。
当然,我们可以看到我已经接入了Shiro。在我try里面的一段,如果找不到用户或者说密码比对失败或者其他异常,我们都可以看作是登录失败!(就算是正确的账号和密码,但是后端出现了异常不能处理,那么我们也有必要阻止用户登录系统)
当我们的程序接收到前端传递过来的用户名和密码后,我们构造了UsernamePasswordToken并且进入了验证流程,那么接着一起看看Shiro的验证过程吧!
/**
* created by 程 2016/11/25
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/*
* 登录信息和用户验证信息验证(non-Javadoc)
* @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken user = (UsernamePasswordToken) token;
LogPrintUtil.getInstance(ShiroRealm.class).logOutLittle("开始登录====>\n用户为:" + user.getUsername());
String userLogin = user.getUsername();
User result4Find;
try {
result4Find = userService.findOneById(userLogin);
} catch (NotFoundException e) {
throw new AuthenticationException("用户不存在!");
}
//进入密码比对器,第一个参数是数据库中存在的用户,第二个参数是数据库中保存的加密密码,第三个参数是realmName直接使用this.getName()就行
return new SimpleAuthenticationInfo(result4Find, result4Find.getUserPass(), this.getName());
}
}
当然这里的代码基本上没啥难度,毕竟就是查找到用户,然后把用户的信息和用户密码传递过去进行比对嘛,基本是毫无难度。
接着进入密码比对器进行密码校验!
/**
* Created by cheng on 17/5/16.
*/
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
@Autowired
private UserService userService;
/**
* 密码比较方法
*
* @param token
* @param info
* @return
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
try {
//从ShiroRealm传递过来的UsernamePasswordToken,UsernamePasswordToken实现AuthenticationToken
UsernamePasswordToken user = (UsernamePasswordToken) token;
String userPass = new String(user.getPassword());
userPass = userPass.toLowerCase(); //将大写md5转换为小写md5
if (userPass.length() > 16 && userPass.length() == 32) { //32位小写转换为16位小写
userPass = userPass.substring(8, 24).toLowerCase();
}
//取出数据库中加密的密码
User result4Find = (User) info.getPrincipals().asList().get(0);
String encryptPassword = EncryptUtils.encryptPassword(userPass, result4Find.getUserActivationKey());
return this.equals(encryptPassword, result4Find.getUserPass());
} catch (Exception e) {
e.printStackTrace();
}
return super.doCredentialsMatch(token, info);
}
}
在上面的密码比对器中,我们可以看到doCredentialsMatch方法中我们最终返回的是一个equals方法的返回值,那么说明我们需要把明文密码转换为密文和数据库密文密码进行比对。我这样说可能大家一点都不信,但是我们看下源码
public class SimpleCredentialsMatcher extends CodecSupport implements CredentialsMatcher {
//···省略若干行代码
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = this.getCredentials(token);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenCredentials, accountCredentials);
}
}
在SimpleCredentialsMatcher中,doCredentialsMatch方法的返回值也是equals,所以说我们上面直接用equals也是没有任何毛病!
在上面密码的比对实现中,可以看到我是先把AuthenticationToken强转为UsernamePasswordToken(关于这个,我们查看源码可以知道其实UsernamePasswordToken实现AuthenticationToken)。然后获取到用户密码经过加密再和数据库密码进行比对。
要是看过我往期项目的童鞋可能会说我前面已经写过一个login的方法在UserServiceImpl中,所以说我们现在需要用我们以前的方法来实现用户登录,那么我们现在需要怎么做呢?
利用以往代码进行登录改进(独家经验!)
首先我们前面代码中的登录是经过检验毫无毛病的正确的登录。代码如下:
/**
* Created by mac on 2016/12/15.
*/
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* 登录用户Service
*
* @param userLogin 用户名
* @param userPass 用户密码
* @return
*/
@Override
public User login(String userLogin, String userPass) throws enterInfoErrorException, NotFoundException {
if (StringUtils.isEmpty(userLogin) || StringUtils.isEmpty(userPass)) {
throw new enterInfoErrorException("用户名和密码不能为空!请检查!");
}
User result = null;
result = findOneById(userLogin);
if (null == result) throw new NotFoundException("用户未找到!");
try {
userPass = userPass.toLowerCase(); //将大写md5转换为小写md5
if (userPass.length() > 16 && userPass.length() == 32) { //32位小写转换为16位小写
userPass = userPass.substring(8, 24).toLowerCase();
}
} catch (Exception e) {
e.printStackTrace();
throw new enterInfoErrorException("密码错误!");
}
String encryptPassword = EncryptUtils.encryptPassword(userPass, result.getUserActivationKey());
if (!encryptPassword.equals(result.getUserPass())) {
throw new enterInfoErrorException("用户名和密码不匹配!");
}
return result;
}
}
我们看上面的登录的服务代码把我们前面的密码比对器这里的都涵盖了,所以说,我们完全可以绕过密码比对器!
OK,思路来了,我们现在需要绕过密码比对器,那么我们接下来该怎么实现呢?
a、获得Subject -> b、构造UsernamePasswordToken -> c、调用Subject.login() -> d、实现AuthorizingRealm并执行认证方法 -> e、在认证方法中调用userService.login(); -> f、实现SimpleCredentialsMatcher并实例化该对象且执行doCredentialsMatch()方法进行密码比对(强行返回true)
所以,我们现在完全不慌,我们现在需要重写的方法仅仅有doCredentialsMatch和doGetAuthenticationInfo。
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
/**
* 密码比较方法
*
* @param token
* @param info
* @return
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
return true;
}
}
/**
* created by 程 2016/11/25
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/*
* 登录信息和用户验证信息验证(non-Javadoc)
* @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken user = (UsernamePasswordToken) token;
LogPrintUtil.getInstance(ShiroRealm.class).logOutLittle("开始登录====>\n用户为:" + user.getUsername());
String userLogin = user.getUsername();
char[] password = user.getPassword();
User loginResult = null;
try {
loginResult = userService.login(userLogin, new String(password));
} catch (enterInfoErrorException | NotFoundException e) {
e.printStackTrace();
LogPrintUtil.getInstance(ShiroRealm.class).logOutLittle("登录异常结束====>\n用户为:" + user.getUsername());
throw new AuthenticationException(e.getMessage());
}
LogPrintUtil.getInstance(ShiroRealm.class).logOutLittle("登录成功====>\n用户为:" + user.getUsername());
return new SimpleAuthenticationInfo(loginResult, user.getPassword(), this.getName());
}
}
当我们的代码重构过后我们可以看到我们的代码更加清晰,同时降低了代码的重复率更重要的是我们接入了Shiro的登录验证!
相对于前面的登录验证,我们接下来接入的是我们的权限管理。
在移动端产品中,我们常见的权限管理就是屏蔽用户的入口,同样的web端也是一样的,所以这里我们的想法也是屏蔽后端的用户入口!
我们这里的思路是什么呢?
a、重写doGetAuthorizationInfo方法 -> b、获取用户数据 -> c、返回用户权限列表
public class ShiroRealm extends AuthorizingRealm {
/*
* 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用,负责在应用程序中决定用户的访问控制的方法(non-Javadoc)
* @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
//获取当前的用户
User result4Find = (User) pc.asList().get(0);
//构造权限列表
SimpleAuthorizationInfo perList = new SimpleAuthorizationInfo();
try {
if (result4Find.getUserActivationKey().equals("admin"))
perList.addStringPermissions(PermissionUtil.getAdminPer());
else perList.addStringPermissions(PermissionUtil.getOtherPer());
} catch (Exception e) {
e.printStackTrace();
perList.addStringPermissions(PermissionUtil.getOtherPer());
}
return perList;
}
}
在上面的代码中,可以明显的看到我的用户权限粗略的分了两类别,一个是admin,一个是其他。所以接着我们可以看下我们具体的权限分为什么些。
public class PermissionUtil {
private final static String POST_CENTER = "文章中心"
, MSG_CENTER = "留言管理"
, MEDIA_CENTER = "多媒体管理"
, API_CENTER = "API系统"
, USER_CENTER = "用户管理"
, WEXIN_CENTER = "微信管理"
, SYSTEM_CENTER = "服务器中心";
/**
* 获取管理员权限
* @return 返回管理员权限集合
*/
public static List getAdminPer(){
List list = new ArrayList<>();
list.add(POST_CENTER);
list.add(MSG_CENTER);
list.add(MEDIA_CENTER);
list.add(API_CENTER);
list.add(USER_CENTER);
list.add(WEXIN_CENTER);
list.add(SYSTEM_CENTER);
return list;
}
public static List getOtherPer(){
List list = new ArrayList<>();
list.add(POST_CENTER);
list.add(MSG_CENTER);
list.add(MEDIA_CENTER);
return list;
}
}
那我们这样配置了后,我们去哪里接入用户权限呢?当然是去用户后端模块的地址列表中。在我的项目中用户功能模块统一配置在 _menuTemplate.jsp中,所以如下:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
可以在上面的菜单列表中看到有类似下面的代码块。
在这种代码块中,name就是我们后端控制的权限名称,同样的这些代码块中的代码也是根据是否存在而决定是否显示该模块,所以简单的权限管理也就到此结束。
总结:
Shiro中的登录验证和权限管理都是需要实现AuthorizingRealm,分别对应了doGetAuthenticationInfo和doGetAuthorizationInfo这两个方法。
密码比对需要实现SimpleCredentialsMatcher中的doCredentialsMatch方法(当然可以直接绕过!!!)
权限管理中,思路是控制页面的功能模块入口的显示与否。主要是利用了Shiro标签!