管理后台没有采用前后端分离,采用shiro+thymeleaf的若依框架,项目模块较多,项目分组较多,有希望进行分布式开发的需求,可以共同探讨下本文的思路。
升级思路
- 编写通用的shiro认证模块,所有服务引入此模块。
- 搭建认证中心portal,用户登录都走认证中心
- 基于session的有效domain,所有服务使用相同域名, shiro通过redis存放session。
思路参见以前文章
通用的shiro认证模块
构建模块ruoyi-dora-starter-web
,其他web项目引入此starter即可引入框架shiro认证。
引入shiro依赖
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
</dependency>
编写shro配置
主要需注入 UserRealm
credentialsMatcher
SessionsSecurityManager
@Configuration
public class ShiroConfiguration {
/**
* 用户认证
**/
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
// 设置密码认证器
userRealm.setCredentialsMatcher(credentialsMatcher());
return userRealm;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
// filterChain 过滤静态资源
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/static/**", "anon");
chainDefinition.addPathDefinition("/ajax/**", "anon");
chainDefinition.addPathDefinition("/css/**", "anon");
chainDefinition.addPathDefinition("/file/**", "anon");
chainDefinition.addPathDefinition("/fonts/**", "anon");
chainDefinition.addPathDefinition("/html/**", "anon");
chainDefinition.addPathDefinition("/i18n/**", "anon");
chainDefinition.addPathDefinition("/img/**", "anon");
chainDefinition.addPathDefinition("/js/**", "anon");
chainDefinition.addPathDefinition("/ruoyi/**", "anon");
chainDefinition.addPathDefinition("/login", "anon");
chainDefinition.addPathDefinition("/captcha", "anon");
chainDefinition.addPathDefinition("/logout", "anon");
chainDefinition.addPathDefinition("/ruoyi.png", "anon");
chainDefinition.addPathDefinition("/favicon.ico", "anon");
chainDefinition.addPathDefinition("/layuiadmin/**", "anon");
chainDefinition.addPathDefinition("/druid/**", "anon");
chainDefinition.addPathDefinition("/api/**", "anon");
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
@Bean
public HashedCredentialsMatcher credentialsMatcher() {
// 密码认证器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// Md5Hash.ALGORITHM_NAME
credentialsMatcher.setHashAlgorithmName("SHA-256");
credentialsMatcher.setStoredCredentialsHexEncoded(false);
credentialsMatcher.setHashIterations(1024);
return credentialsMatcher;
}
@Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
return securityManager;
}
认证授权 realm
为验证可行性,认证授权暂时写定
@Slf4j
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) {
// 角色 权限信息 暂时写定
User user = (User) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet();
Set<String> permissions = new HashSet();
if ("admin".equals(user.getUserName())) {
roles.add("admin");
permissions.add("op:write");
} else {
roles.add("user");
permissions.add("op:read");
}
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String)authenticationToken.getPrincipal();
String credentials = new String((char[])authenticationToken.getCredentials());
User user = new User();
user.setUserName(username);
String password = credentials;
// 此处暂时跳过密码验证 与注入的bean credentialsMatcher算法一致,算出 hashedCredentials
// 实际需从数据库中取出
String salt = "salt";
int hashIterations = 1024;
String encodedPassword = (new SimpleHash("SHA-256", password, Util.bytes(salt), hashIterations)).toBase64();
log.info("password: {} encode: {}",password,encodedPassword);
user.setPassword(encodedPassword);
// authenticationToken.getCredentials() + salt 经credentialsMatcher加密 与 hashedCredentials 比较
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), Util.bytes(salt), this.getName());
return authenticationInfo;
}
Starter 自动配置
META-INF文件下spring-factories加入配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ruoyi.dora.web.config.DoraWebAutoConfiguration,\
com.ruoyi.dora.web.config.ShiroConfiguration
至此认证模块starter完成
搭建认证中心
构建独立项目 ruoyi-dora-portal-web
引入ruoyi-dora-starter-web
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-dora-starter-web</artifactId>
</dependency>
引入若依前端资源文件
引入若依admin前端的静态资源文件 static包、templates包所有文件
登录控制
@Slf4j
@Controller
public class LoginController {
@GetMapping("/login")
public String loginPage (Model model) {
if(SecurityUtils.getSubject().isAuthenticated()){
return "redirect:/index";
}
// 若依框架的配置 临时处理
Map<String, Object> configMap = new HashMap<>();
configMap.put("sys.account.registerUser", true);
model.addAttribute("config", configMap);
model.addAttribute("captchaEnabled", false);
return "login";
}
@GetMapping("/logout")
public String logout () {
SecurityUtils.getSubject().logout();
return "redirect:/login";
}
@PostMapping("/login")
@ResponseBody
public AjaxResult ajaxLogin (String username, String password, Boolean rememberMe) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return success();
} catch (AuthenticationException e) {
log.error("login error.",e);
String msg = "用户或密码错误";
if (StringUtils.isNotEmpty(e.getMessage())) {
msg = e.getMessage();
}
return error(msg);
}
}
@GetMapping("/unauth")
public String unauth () {
return "error/unauth";
}
}
主页index
主要获取了数据库menu菜单,及UI风格设置。ISysMenuService
采用了mybatis-plus的generator生成器代码风格,menuService.selectMenusByUser
参见若依原代码,或本文gitee代码。 对一些代码做了临时处理,直接写定。
@Slf4j
@Controller
public class PortalController {
@Autowired
private ISysUserService sysUserService;
@Autowired
private ISysMenuService menuService;
@Autowired
private ISysConfigService configService;
@GetMapping({"/","/index"})
public String index(ModelMap modelMap){
Object principal = SecurityUtils.getSubject().getPrincipal();
log.info("principal {}",principal.toString());
SysUser user = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getLoginName, "admin"), false);
List<SysMenu> menus = menuService.selectMenusByUser(user);
modelMap.put("menus", menus);
modelMap.put("user", user);
modelMap.put("sideTheme", configService.selectConfigByKey("sys.index.sideTheme").getConfigValue());
modelMap.put("skinName", configService.selectConfigByKey("sys.index.skinName").getConfigValue());
modelMap.put("ignoreFooter", configService.selectConfigByKey("sys.index.ignoreFooter").getConfigValue());
// 配置临时处理
// modelMap.put("copyrightYear", RuoYiConfig.getCopyrightYear());
// modelMap.put("demoEnabled", RuoYiConfig.isDemoEnabled());
// modelMap.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
// modelMap.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
modelMap.put("copyrightYear", "2021");
modelMap.put("demoEnabled", "true");
modelMap.put("isDefaultModifyPwd", false);
modelMap.put("isPasswordExpired", false);
// 菜单导航显示风格
// String menuStyle = configService.selectConfigByKey("sys.index.menuStyle");
String menuStyle = "default";
// 移动端,默认使左侧导航菜单,否则取默认配置
String indexStyle = menuStyle;
//ServletUtils.checkAgentIsMobile(ServletUtils.getRequest().getHeader("User-Agent")) ? "index" : menuStyle;
// 优先Cookie配置导航菜单
// Cookie[] cookies = ServletUtils.getRequest().getCookies();
// for (Cookie cookie : cookies)
// {
// if (StringUtils.isNotEmpty(cookie.getName()) && "nav-style".equalsIgnoreCase(cookie.getName()))
// {
// indexStyle = cookie.getValue();
// break;
// }
// }
if("topnav".equalsIgnoreCase(indexStyle)){
return "index-topnav";
}
return "index";
}
/**
* UI 工作区域frame main
**/
@GetMapping("/system/main")
public String sysMain(Model model){
model.addAttribute("version","0.0.1");
return "main";
}
}
至此启动项目,即可进行登录,并进入到index首页(菜单项具体功能并未实现,可参见若依原码)。
redis存放session
对于多端服务将session统一管理。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
yaml 配置redis
启动类添加注解@EnableRedisHttpSession
将session存放在redis
spring:
datasource:
url: jdbc:p6spy:mysql://localhost:3306/ry?useSSL=false&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
#redis
redis:
host: localhost
port: 6379
栗子sky-web
建立独立项目 demo-sky-web
引入上面的依赖ruoyi-dora-starter-web
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-dora-starter-web</artifactId>
</dependency>
将shrio的登录地址指向认证中心ruoyi-dora-portal-web
的地址
配置reids, 启动类添加注解@EnableRedisHttpSession
将session存放在redis
shiro:
loginUrl: http://localhost:8600/login
spring:
redis:
host: localhost
port: 6379
编写简单测试页面
@Controller public class SkyController {
@GetMapping("/sky") public String skyPage(){ return "sky"; }
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sky</title>
</head>
<body>
<h1>Sky sky</h1>
</body>
</html>
数据库添加菜单, 将菜单地址指向 demo-sky的地址全路径http://localhost:8080/sky,要求域名相同(此处域名为localhost)
INSERT INTO `ry`.`sys_menu`(`menu_id`, `menu_name`, `parent_id`, `order_num`, `url`, `target`, `menu_type`, `visible`, `is_refresh`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (510, '天天业务管理', 5, 1, 'http://localhost:8080/sky', '', 'C', '0', '1', 'system:sky:view', 'fa fa-user-o', 'admin', '2021-07-01 02:17:28', '', NULL, '天天业务管理菜单');
直接访问 http://localhost:8080/sky> 跳转到登录页,登录后跳转到主页index,访问菜单天天业务可以正常访问。
至此基于若依框架 shiro+thymeleaf的分布式项目可行性探索完成,后续工作ruoyi-dora-starter-web
中UserReaml 用户信息、角色、权限信息可以动态获取 如通过http、openfeign调用user服务获取。若依原框架功能未完全迁移,按需参见原框架。
总结
- 利用session的domain作用域,使用相同域名,及redis统一管理session, 进行分布式session管理。
- 抽象出一个公用start进行shiro的配置,业务模块引入start即可使用。
- 所有模块的登录页面都指向统一的登录认证中心。
若对此方面感兴趣,可留言扣1,待功能完善后回复评论。或参见gitee代码ruoyi-dora共同探讨研究。
项目Gitee ruoyi-dora (dora 源自动画片--爱探险的朵拉)
若依是一个优秀的后台框架。