开发需求:Jeesite登录模块添加用户类型选择,并且多添加一个前段角色,验证并授权,允许前段角色登录
数据表
使用的数据库为MySql,拓展数据库管理工具为Navicat
在Jeesite搭建完成的基础上,首先从数据库表的建设开始,后台用户表已经有了,这里建立的是前台用户表,前后用户分离,参照后台用户表的字段信息。
前台用户表 front_user
省略代码生成部分
前后台路径设置分析及框架原设计原理
路径设置分析
提示,输入以下几个地址尝试访问:
输入“http://localhost/项目名”, 会直接跳转到项目工程的默认首页,一般在web.xml中设定对应的首页,但这里通过spring-mvc.xml的81行可见设置,默认首页映射到jeesite.properties的web.view.index属性中,所以,我们直接配置这个属性就可以完成默认首页的设置。
接下来,我们看一下原设定是什么,web.view.index属性原设定值为“/a”,“/a”是什么,它是“后台管理”路径的前缀路径,用来区分前后台的路径,我们在默认登录的时候会进入到“/a/login”的路径中,这个值“/a”对应的属性为jeesite.properties的“adminPath”,参照在controller层中类文件可见,映射路径@RequestMapping中必带“${adminPath}”参数,所以我们可以确定,凡是带了该参数的均为后台路径(jeesite.properties源代码模式下unicode编码已经有说明了)。
adminPath=/a
frontPath=/f
web.view.index=/a
所以实际的流程是这样的,通过输入“http://localhost/项目名”地址访问, 会跳转到web.view.index设定的首页地址(这里以原设定“/a”为例),所以访问的实际地址为“http://localhost/项目名/a”, 而这个地址对应的controller层方法是在“com.thinkgem.jeesite.modules.sys.web.LoginController”类文件中,对应的方法为“index”。
/**
* 登录成功,进入管理首页
*/
@RequiresPermissions("user")
@RequestMapping(value = "${adminPath}")
public String index(HttpServletRequest request, HttpServletResponse response) {
}
输入“http://localhost/项目名”或“http://localhost/项目名/a”会来到这个方法, 但是打印输出发现并没有进来,而且最终地址为“http://localhost/项目名/a/login”, 是因为在进入这个方法前要进行shiro权限校验,如果没通过是无法进入会跳转到指定的地址,参照对比spring-context-shiro.xml,即可发现。
/test/** = anon
/static/** = anon
/userfiles/** = anon
${adminPath}/cas = cas
${adminPath}/login = authc
${adminPath}/logout = logout
${adminPath}/** = user
/act/editor/** = user
/ReportServer/** = user
进入“index”方法需要“user”权限(进行shiro权限校验),否则会跳转到“loginUrl”对应的方法“${adminPath}/login”,即“http://localhost/项目名/a/login”, 至此后台的登录流程大概。
框架原设计原理
sysLogin.jsp登录:
整个jsp页面其实就是一个表单,它的主要目的就是接受用户输入的用户名和密码字段,然后交给后台处理。action变量指定了该表达的提交方式,既是交由/a/login所对应的函数来处理。
表单里的两个属性,一个属性名为username,一个名为password,表单会借由request属性传到函数当中,届时就能通过getUsername和getPassword两个函数从request中取出。这部分是在FormAuthenticationFilter中的createToken函数中实现。
shiro
jsp将username和password打包扔给后台,那么后台是由什么接受呢?在spring-mvc中,负责接受前台数据的是controller部分,而form中所指定的action是/a/login。
LoginController:第三步
/**
* 管理登录
*/
@RequestMapping(value = "${adminPath}/login", method = RequestMethod.GET)
public String login(HttpServletRequest request, HttpServletResponse response, Model model) {
Principal principal = UserUtils.getPrincipal();
if (logger.isDebugEnabled()){
logger.debug("login, active session size: {}", sessionDAO.getActiveSessions(false).size());
}
// 如果已登录,再次访问主页,则退出原账号。
if (Global.TRUE.equals(Global.getConfig("notAllowRefreshIndex"))){
CookieUtils.setCookie(response, "LOGINED", "false");
}
// 如果已经登录,则跳转到管理首页
if(principal != null && !principal.isMobileLogin()){
return "redirect:" + adminPath;
}
}
/**
* 登录失败,真正登录的POST请求由Filter完成
*/
@RequestMapping(value = "${adminPath}/login", method = RequestMethod.POST)
public String loginFail(HttpServletRequest request, HttpServletResponse response, Model model) {
// 通过 tpye 类型来区分实例化 Principal 是 User 或者 FrontUser
String type = request.getParameter("type").toString();
Principal principal;
if(type == "0"){
principal = UserUtils.getPrincipal();
}else {
principal = FrontUserUtils.getPrincipal();
}
//Principal principal = UserUtils.getPrincipal();
// 如果已经登录,则跳转到管理首页
if(principal != null){
return "redirect:" + adminPath;
}
String username = WebUtils.getCleanParam(request, FormAuthenticationFilter.DEFAULT_USERNAME_PARAM);
boolean rememberMe = WebUtils.isTrue(request, FormAuthenticationFilter.DEFAULT_REMEMBER_ME_PARAM);
boolean mobile = WebUtils.isTrue(request, FormAuthenticationFilter.DEFAULT_MOBILE_PARAM);
String exception = (String)request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
String message = (String)request.getAttribute(FormAuthenticationFilter.DEFAULT_MESSAGE_PARAM);
if (StringUtils.isBlank(message) || StringUtils.equals(message, "null")){
message = "用户或密码错误, 请重试.";
}
model.addAttribute(FormAuthenticationFilter.DEFAULT_USERNAME_PARAM, username);
model.addAttribute(FormAuthenticationFilter.DEFAULT_REMEMBER_ME_PARAM, rememberMe);
model.addAttribute(FormAuthenticationFilter.DEFAULT_MOBILE_PARAM, mobile);
model.addAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME, exception);
model.addAttribute(FormAuthenticationFilter.DEFAULT_MESSAGE_PARAM, message);
if (logger.isDebugEnabled()){
logger.debug("login fail, active session size: {}, message: {}, exception: {}",
sessionDAO.getActiveSessions(false).size(), message, exception);
}
return "modules/sys/sysLogin";
}
这是因为shiro的登陆功能在controller之前加入了一个filter。这个filter被配置在文件spring-context-shiro.xml文件里。
/static/** = anon
/userfiles/** = anon
${adminPath}/login = authc
${adminPath}/logout = logout
${adminPath}/** = user
以上就是配置过程最关键的部分。loginuUrl属性所指定的url表示的是所有未通过验证的url所访问的位置,此处就是登录界面;successUrl表示的是成功登陆的url访问的位置,此处就是主页。filters则是配置具体验证方法的位置。在此处,${adminPath}/login = authc指定了/a/login,既登陆页面,所需要的验证权限名为authc,又配置了authc所用的filter为formAuthenticationFilter。
因此整个逻辑是:如果任何地方未登陆,则访问/a/login页面,而/a/login页面的验证权限中又指定了formAuthenticationFilter做为过滤,如果过滤中验证成功,则访问/a这个主页。所以,login.jsp中的表单信息则首先交由formAuthenticationFilter首先处理。
formAuthenticationFilter中的处理,需要关注的类主要在com.thinkgem.jeesite.modules.sys.security这个包里。通常FormAuthenticationFilter是主要逻辑管理类,SystemAuthorizingRealm这个类则是数据处理类,相当于DAO。但是直接从代码里没发看出功能,这是因为jeesite中的这两个类都是继承于shiro的类。
大致来讲,首先表单的request被formAuthenticationFilter接收到,然后传给createToken函数,该函数从request中取出name和password,然后生成自定义的一个token传给SystemAuthorizingRealm中的doGetAuthenticationInfo验证。SystemAuthorizingRealm中有systemService的实例,该实例含有userDAO能取出数据库中的name和password。接着由这两个密码生成SimpleAuthenticationInfo,再由info中的逻辑来验证。以上过程中jeesite实现的代码分别如下:
FormAuthenticationFilter:第一步
// TODO 设置 tyoe 常量
public static final String DEFAULT_TYPE = "type";
// TODO 设置 tyoe 常量
private String typeParam = DEFAULT_TYPE;
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String username = getUsername(request);
String password = getPassword(request);
// TODO 通过请求域获得 type
// 通过请求域获得 type
String type = getType(request);
if (password==null){
password = "";
}
boolean rememberMe = isRememberMe(request);
String host = StringUtils.getRemoteAddr((HttpServletRequest)request);
String captcha = getCaptcha(request);
boolean mobile = isMobileLogin(request);
//返回一个 UsernamePasswordToken 令牌对象进行校验
return new UsernamePasswordToken(username, password.toCharArray(),type, rememberMe, host, captcha, mobile);
}
/**
* 获取登录用户名
*/
protected String getUsername(ServletRequest request, ServletResponse response) {
String username = super.getUsername(request);
if (StringUtils.isBlank(username)){
username = StringUtils.toString(request.getAttribute(getUsernameParam()), StringUtils.EMPTY);
}
System.out.println(" 11111 ========================================================"+username);
return username;
}
/**
* 获取登录密码
*/
@Override
protected String getPassword(ServletRequest request) {
String password = super.getPassword(request);
if (StringUtils.isBlank(password)){
password = StringUtils.toString(request.getAttribute(getPasswordParam()), StringUtils.EMPTY);
}
System.out.println(" 11111 ========================================================"+password);
return password;
}
// TODO 获取用户类型
/**
* 获取用户类型
*/
protected String getType(ServletRequest request){
String type = this.getTypeParam(request);
if (StringUtils.isBlank(type)){
type = StringUtils.toString(request.getAttribute(getTypeParam()),StringUtils.EMPTY);
}
System.out.println(" 11111 ========================================================"+type);
return type;
}
// TODO TypeParam 的 get 方法
public String getTypeParam(){
return typeParam;
}
// TODO TypeParam 的 get 方法(带 ServletRequest 请求对象 )
protected String getTypeParam(ServletRequest request){
return WebUtils.getCleanParam(request,getTypeParam());
}
UsernamePasswordToken:
// TODO 设置用户类型 type
private String type;
// TODO 带用户类型 type 的构造器
public UsernamePasswordToken(String username, char[] password,
String type, boolean rememberMe, String host, String captcha, boolean mobileLogin) {
super(username, password, rememberMe, host);
this.type = type;
this.captcha = captcha;
this.mobileLogin = mobileLogin;
}
// TODO
public String getType() {
return type;
}
// TODO
public void setType(String type) {
this.type = type;
}
SystemService:
//TODO 根据登录名获取前端用户
/**
* 根据登录名获取前端用户
* */
public CampusinfoFrontUser getFrontUserByLoginName(String loginName){
return FrontUserUtils.getByFrontLoginName(loginName);
}
SystemAuthorizingRealm:第二步
/**
* 认证回调函数, 登录时调用
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
int activeSessionSize = getSystemService().getSessionDao().getActiveSessions(false).size();
if (logger.isDebugEnabled()){
logger.debug("login submit, active session size: {}, username: {}", activeSessionSize, token.getUsername());
}
// 校验登录验证码
if (LoginController.isValidateCodeLogin(token.getUsername(), false, false)){
Session session = UserUtils.getSession();
String code = (String)session.getAttribute(ValidateCodeServlet.VALIDATE_CODE);
if (token.getCaptcha() == null || !token.getCaptcha().toUpperCase().equals(code)){
throw new AuthenticationException("msg:验证码错误, 请重试.");
}
}
System.out.println(" 22222 ========================================================"+ token.getType());
String type = token.getType();
// 判断用户类型
if(type.equals("0")){
// 校验后端用户名密码
User user = getSystemService().getUserByLoginName(token.getUsername());
if (user != null) {
// 校验后台用户是否允许登陆
if (Global.NO.equals(user.getLoginFlag())){
throw new AuthenticationException("msg:该已帐号禁止登录.");
}
byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
return new SimpleAuthenticationInfo(new Principal(user, token.isMobileLogin()),
user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName());
} else {
return null;
}
} else{
System.out.println(" campusinfoFrontUser +++++++++++++++++++++++++++++++++++++++++++++++++++++");
// 校验前端用户名密码
CampusinfoFrontUser campusinfoFrontUser = getSystemService().getFrontUserByLoginName(token.getUsername());
if(campusinfoFrontUser != null){
//TODO 校验前端用户是否允许登陆
// 校验前端用户是否允许登陆
if (Global.NO.equals(campusinfoFrontUser.getLoginFlag())){
throw new AuthenticationException("msg:该前端帐号禁止登录.");
}
byte[] salt_Front = Encodes.decodeHex(campusinfoFrontUser.getPassword().substring(0,16));
return new SimpleAuthenticationInfo(new Principal(campusinfoFrontUser, token.isMobileLogin()),
campusinfoFrontUser.getPassword().substring(16),ByteSource.Util.bytes(salt_Front),getName());
}else{
return null;
}
}
}
/**
* 授权用户信息
*/
public static class Principal implements Serializable {
private static final long serialVersionUID = 1L;
private String id; // 编号
private String loginName; // 登录名
private String name; // 姓名
private boolean mobileLogin; // 是否手机登录
//private Map cacheMap;
//TODO 前端用户授权构造器方法
// 前端用户授权构造器
public Principal(CampusinfoFrontUser campusinfoFrontUser, boolean mobileLogin){
this.id = campusinfoFrontUser.getId();
this.loginName = campusinfoFrontUser.getLoginName();
this.name = campusinfoFrontUser.getName();
this.mobileLogin = mobileLogin;
}
// 后台用户授权构造器
public Principal(User user, boolean mobileLogin) {
this.id = user.getId();
this.loginName = user.getLoginName();
this.name = user.getName();
this.mobileLogin = mobileLogin;
}
public String getId() {
return id;
}
public String getLoginName() {
return loginName;
}
public String getName() {
return name;
}
public boolean isMobileLogin() {
return mobileLogin;
}
//@JsonIgnore
//public Map getCacheMap() {
//if (cacheMap==null){
//cacheMap = new HashMap();
//}
//return cacheMap;
//}
/**
* 获取SESSIONID
*/
public String getSessionid() {
try{
return (String) UserUtils.getSession().getId();
}catch (Exception e) {
return "";
}
}
@Override
public String toString() {
return id;
}
}
FrontUserUtils:
/**
* Created by yt on 2017/10/26.
* 前端用户工具类
*/
public class FrontUserUtils {
//TODO 前端用户DAO
//前端用户DAO
private static CampusinfoFrontUserDao campusinfoFrontUserDao = SpringContextHolder.getBean(CampusinfoFrontUserDao.class);
private static RoleDao roleDao = SpringContextHolder.getBean(RoleDao.class);
public static final String FRONTUSER_CACHE = "frontuserCache";
public static final String USER_CACHE_ID_ = "id_";
public static final String USER_CACHE_LOGIN_NAME_ = "ln";
//TODO 根据前端登录名获取用户
/**
* 根据前端登录名获取用户
* @param loginName
* @return 取不到返回null
*/
public static CampusinfoFrontUser getByFrontLoginName(String loginName){
CampusinfoFrontUser campusinfoFrontUser = (CampusinfoFrontUser)CacheUtils.get(FRONTUSER_CACHE,USER_CACHE_LOGIN_NAME_ + loginName);
if (campusinfoFrontUser == null) {
campusinfoFrontUser = campusinfoFrontUserDao.getByFrontLoginName(new CampusinfoFrontUser(null, loginName));
if (campusinfoFrontUser == null) {
return null;
}
campusinfoFrontUser.setRoleList(roleDao.findList(new Role(campusinfoFrontUser)));
CacheUtils.put(FRONTUSER_CACHE, USER_CACHE_ID_ + campusinfoFrontUser.getId(), campusinfoFrontUser);
CacheUtils.put(FRONTUSER_CACHE, USER_CACHE_LOGIN_NAME_ + campusinfoFrontUser.getLoginName(), campusinfoFrontUser);
}
return campusinfoFrontUser;
}
//TODO 获取当前登录者对象
/**
* 获取当前登录者对象
*/
public static Principal getPrincipal(){
try{
Subject subject = SecurityUtils.getSubject();
Principal principal;
principal = (Principal)subject.getPrincipal();
if (principal != null){
return principal;
}
//subject.logout();
}catch (UnavailableSecurityManagerException e) {
}catch (InvalidSessionException e){
}
return null;
}
CampusinfoFrontUserDao:
/**
* 根据前端登录名称查询用户
* @param loginName
* @return CampusinfoFrontUser 对象
*/
public CampusinfoFrontUser getByFrontLoginName(CampusinfoFrontUser campusinfoFrontUser);
CampusinfoFrontUserDao.xml:
RoleDao.xml: