add: 重新实现starter-thymeleaf,并添加thymeleaf+shiro的demo

refer: https://nutz.cn/yvr/t/7gareesackiftr1bgo2tadmobq
This commit is contained in:
Wendal Chen 2019-04-17 19:38:16 +08:00
parent 6d35aef3e3
commit 63ac1ff3ed
13 changed files with 508 additions and 56 deletions

View File

@ -0,0 +1,60 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-demo-simple</artifactId>
<version>2.3.5-SNAPSHOT</version>
</parent>
<artifactId>nutzboot-demo-simple-thymeleaf-shiro</artifactId>
<dependencies>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-nutz-mvc</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-nutz-dao</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-shiro</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,60 @@
package io.nutz.demo.simple;
import java.util.Date;
import javax.servlet.http.HttpSession;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.subject.Subject;
import org.nutz.boot.NbApp;
import org.nutz.dao.Dao;
import org.nutz.dao.util.Daos;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import io.nutz.demo.simple.bean.User;
@IocBean
public class MainLauncher {
@Inject
protected Dao dao;
@At({"/", "/index"})
@Ok("th:/index.html")
public NutMap index() {
return NutMap.NEW().setv("name", "NB").setv("age", 18);
}
@Ok("raw")
@At("/shiro/test")
public boolean isAuthenticated(HttpSession session) {
Subject subject = SecurityUtils.getSubject();
return subject.isAuthenticated();
}
public void init() {
Daos.createTablesInPackage(dao, User.class, false);
dao.insert(newUser("admin", "123456"));
dao.insert(newUser("wendal", "123123"));
}
protected static User newUser(String name, String password) {
User user = new User();
user.setName(name);
user.setSalt(R.UU32());
user.setPassword(new Sha256Hash(password, user.getSalt()).toHex());
user.setCreateTime(new Date());
return user;
}
public static void main(String[] args) throws Exception {
new NbApp().run();
}
}

View File

@ -0,0 +1,50 @@
package io.nutz.demo.simple.bean;
import java.util.Date;
import org.nutz.dao.entity.annotation.Id;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
@Table("t_user")
public class User {
@Id
private long id;
@Name
private String name;
private String password;
private String salt;
private Date createTime;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}

View File

@ -0,0 +1,58 @@
package io.nutz.demo.simple.module;
import javax.servlet.http.HttpSession;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.subject.Subject;
import org.nutz.dao.Dao;
import org.nutz.integration.shiro.SimpleShiroToken;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Fail;
import org.nutz.mvc.annotation.GET;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.POST;
import org.nutz.mvc.annotation.Param;
import io.nutz.demo.simple.bean.User;
@At("/user")
@IocBean
public class UserModule {
@Inject
Dao dao;
@GET
@At("/login")
@Ok(">>:/login.html")
public void loginPage() {}
@Ok("json")
@Fail("http:500")
@POST
@At("/login")
public boolean login(@Param("username")String username, @Param("password")String password, HttpSession session) {
User user = dao.fetch(User.class, username);
if (user == null)
return false;
Sha256Hash hash = new Sha256Hash(password, user.getSalt());
if (!hash.toHex().equals(user.getPassword())) {
return false;
}
Subject subject = SecurityUtils.getSubject();
subject.login(new SimpleShiroToken(user.getId()));
return true;
}
@Ok(">>:/index.html")
@At
public void logout() {
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated())
subject.logout();
}
}

View File

@ -0,0 +1,66 @@
package io.nutz.demo.simple.shiro;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAccount;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.subject.PrincipalCollection;
import org.nutz.integration.shiro.AbstractSimpleAuthorizingRealm;
import org.nutz.integration.shiro.SimpleShiroToken;
import org.nutz.ioc.loader.annotation.IocBean;
import io.nutz.demo.simple.bean.User;
@IocBean(name="shiroRealm", fields="dao")
public class SimpleAuthorizingRealm extends AbstractSimpleAuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// null usernames are invalid
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
long userId = ((Number) principals.getPrimaryPrincipal()).longValue();
User user = dao().fetch(User.class, userId);
if (user == null)
return null;
SimpleAuthorizationInfo auth = new SimpleAuthorizationInfo();
auth.addRole(user.getName());
auth.addStringPermission("user:list");
return auth;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
SimpleShiroToken upToken = (SimpleShiroToken) token;
User user = dao().fetch(User.class, (Long)upToken.getPrincipal());
if (user == null)
return null;
return new SimpleAccount(user.getId(), user.getPassword(), getName());
}
public SimpleAuthorizingRealm() {
this(null, null);
}
public SimpleAuthorizingRealm(CacheManager cacheManager, CredentialsMatcher matcher) {
super(cacheManager, matcher);
setAuthenticationTokenClass(SimpleShiroToken.class);
}
public SimpleAuthorizingRealm(CacheManager cacheManager) {
this(cacheManager, null);
}
public SimpleAuthorizingRealm(CredentialsMatcher matcher) {
this(null, matcher);
}
}

View File

@ -0,0 +1,11 @@
server.port=8080
server.host=0.0.0.0
jdbc.url=jdbc:h2:mem:~
shiro.ini.urls:
/rs = anon
/home.html = authc
#end
thymeleaf.dialects=nz.net.ultraq.thymeleaf.LayoutDialect
thymeleaf.dialects.shiro=at.pollux.thymeleaf.shiro.dialect.ShiroDialect

View File

@ -0,0 +1,8 @@
log4j.rootLogger=debug,Console
log4j.logger.org.eclipse.jetty=info
log4j.logger.org.thymeleaf=debug
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=[%-5p] %d{HH:mm:ss.SSS} %l - %m%n

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户个人主页</title>
</head>
<body>
<h2>登录成功才能看到我,<a href="user/logout">现在登出</a></h2>
</body>
</html>

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Shiro测试页</title>
</head>
<body>
<div>
<h2>
<a href="home.html?_=1">点我,访问一个未授权路径,会跳转到登录页面</a>
</h2>
</div>
<div>
<h2>
当前登录状态: <a shiro:authenticated href="#">已登录</a> <a shiro:guest href="#">未登录</a>
</h2>
</div>
</body>
</html>

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>这是登录页面</title>
</head>
<body>
<form action="user/login" id="user_login_form">
账号<input name="username" value="admin">
密码<input name="password" type="password" value="123456">
<input type="submit" id="user_login">
</form>
</body>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>
<script type="text/javascript">
$(function() {
$("#user_login").click(function() {
$.ajax({
type : "POST",
url : "user/login",
data : $("#user_login_form").serialize(),
dataType : "json",
success : function(re) {
if (re) {
window.location = "/home.html";
}
else {
alert("登录失败,用户名应该是admin或者wendal,密码是123456");
}
}
});
return false;
});
});
</script>
</html>

View File

@ -58,7 +58,7 @@
<module>nutzboot-demo-simple-ftp</module>
<module>nutzboot-demo-simple-fastdfs</module>
<module>nutzboot-demo-simple-sqlxmltpl</module>
<module>nutzboot-demo-simple-thymeleaf-shiro</module>
</modules>
<dependencies>

View File

@ -1,50 +1,32 @@
package org.nutz.boot.starter.thymeleaf;
import java.lang.reflect.InvocationTargetException;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.nutz.lang.util.Context;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.Mvcs;
import org.nutz.mvc.view.AbstractPathView;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
public class ThymeleafView extends AbstractPathView {
private static final Log log = Logs.get();
private TemplateEngine templateEngine = new TemplateEngine();
protected TemplateEngine templateEngine;
private String contentType;
private String encoding;
public ThymeleafView(ClassLoader classLoader, NutMap prop, String path) {
protected String contentType;
protected String encoding;
public ThymeleafView(TemplateEngine templateEngine, String path, String contentType, String encoding) {
super(path);
templateEngine.setTemplateResolver(initializeTemplateResolver(classLoader, prop));
encoding = prop.getString("encoding", "UTF-8");
contentType = prop.getString("contentType", "text/html") + "; charset=" + encoding;
prop.getList("dialects", String.class).forEach(dialect -> {
try {
Object obj = Class.forName(dialect).getDeclaredConstructor().newInstance();
if (obj instanceof IDialect) {
log.debugf("在 thymeleaf 视图中加载 %s 类。", dialect);
templateEngine.setDialect((IDialect) obj);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) {
log.debugf("未在 thymeleaf 视图中加载 %s 类,因为其并不是 org.thymeleaf.dialect.IDialect 合法的实现类。", dialect);
}
});
this.templateEngine = templateEngine;
this.contentType = contentType;
this.encoding = encoding;
}
@Override
@ -65,17 +47,4 @@ public class ThymeleafView extends AbstractPathView {
throw e;
}
}
private ITemplateResolver initializeTemplateResolver(ClassLoader classLoader, NutMap prop) {
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(classLoader);
templateResolver.setTemplateMode(prop.getString("mode", "HTML"));
templateResolver.setPrefix(prop.getString("prefix", "template/"));
templateResolver.setSuffix(prop.getString("suffix", ".html"));
templateResolver.setCharacterEncoding(prop.getString("encoding", "UTF-8"));
templateResolver.setCacheable(prop.getBoolean("cache", true));
templateResolver.setCacheTTLMs(prop.getLong("cacheTTLMs", 3600000L));
return templateResolver;
}
}

View File

@ -1,25 +1,64 @@
package org.nutz.boot.starter.thymeleaf;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.io.File;
import org.nutz.boot.AppContext;
import org.nutz.boot.annotation.PropDoc;
import org.nutz.ioc.Ioc;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.Mirror;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.View;
import org.nutz.mvc.ViewMaker;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.FileTemplateResolver;
@IocBean(name="$views_thymeleaf", create="init")
@IocBean(name = "$views_thymeleaf", create = "init")
public class ThymeleafViewMakerStarter implements ViewMaker {
private static final Log log = Logs.get();
public static String PRE = "thymeleaf.";
@PropDoc(value = "渲染模式", defaultValue = "html")
public static final String PROP_MODE = PRE + "mode";
@PropDoc(value = "路径前缀", defaultValue = "template/")
public static final String PROP_PREFIX = PRE + "prefix";
@PropDoc(value = "模板文件后缀", defaultValue = ".html")
public static final String PROP_SUFFIX = PRE + "suffix";
@PropDoc(value = "模板文件编码", defaultValue = "UTF-8")
public static final String PROP_ENCODING = PRE + "encoding";
@PropDoc(value = "启用模板缓存", defaultValue = "true")
public static final String PROP_CACHE_ENABLE = PRE + "cache.enable";
@PropDoc(value = "模板缓存生存时长", defaultValue = "3000")
public static final String PROP_CACHE_TTL_MS = PRE + "cache.ttl";
@PropDoc(value = "模板目录的绝对路径,若不存在,回落到'模板目录的路径'")
public static final String PROP_TEMPLATE_ROOT_LOCAL = PRE + "resolver.rootLocal";
@PropDoc(value = "加载dialects,需要完整类名,逗号分隔")
public static final String PROP_DIALECTS = PRE + "dialects";
@PropDoc(value = "带前缀加载dialect,需要完整类名,逗号分隔")
public static final String PROP_DIALECTS_XXX = PRE + "dialects.xx";
@PropDoc(value = "响应的默认类型", defaultValue="text/html")
public static final String PROP_CONTENT_TYPE = PRE + "contentType";
@Inject
protected PropertiesProxy conf;
@ -28,29 +67,95 @@ public class ThymeleafViewMakerStarter implements ViewMaker {
protected NutMap prop = NutMap.NEW();
protected TemplateEngine templateEngine = new TemplateEngine();
protected String contentType;
protected String encoding;
protected AbstractConfigurableTemplateResolver setupTemplateResolver(AbstractConfigurableTemplateResolver templateResolver) {
templateResolver.setTemplateMode(conf.get(PROP_MODE, "HTML"));
templateResolver.setPrefix(conf.get(PROP_PREFIX, "template/"));
templateResolver.setSuffix(conf.get(PROP_SUFFIX, ".html"));
templateResolver.setCharacterEncoding(encoding);
templateResolver.setCacheable(conf.getBoolean(PROP_CACHE_ENABLE, true));
templateResolver.setCacheTTLMs(conf.getLong(PROP_CACHE_TTL_MS, 3000L));
return templateResolver;
}
protected ClassLoaderTemplateResolver createClassLoaderTemplateResolver() {
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(appContext.getClassLoader());
setupTemplateResolver(templateResolver);
return templateResolver;
}
protected FileTemplateResolver createFileTemplateResolver(String root) {
FileTemplateResolver templateResolver = new FileTemplateResolver();
setupTemplateResolver(templateResolver);
templateResolver.setPrefix(root);
templateResolver.setCacheable(false);
return templateResolver;
}
public void init() {
if (conf == null) {
return;
}
log.debug("thymeleaf init ....");
contentType = conf.get(PROP_CONTENT_TYPE, "text/html");
encoding = conf.get(PROP_ENCODING, "UTF-8");
if (conf.has("thymeleaf.dialects")) {
addDialect(null, conf.get("thymeleaf.dialects"));
}
for (String key : conf.keySet()) {
if (key.startsWith("thymeleaf.")) {
if (key.startsWith("thymeleaf.dialects.")) {
prop.put("dialects", Arrays.stream(conf.get(key).split(",")).map(Strings::trim).collect(Collectors.toList()));
} else {
prop.put(key.substring("thymeleaf.".length()), conf.get(key));
}
if (key.startsWith("thymeleaf.dialects.")) {
String prefix = key.substring("thymeleaf.dialects.".length());
if ("default".equals(prefix))
prefix = null;
String dialects = conf.get(key);
addDialect(prefix, dialects);
}
}
if (conf.has(PROP_TEMPLATE_ROOT_LOCAL)) {
String path = conf.get(PROP_TEMPLATE_ROOT_LOCAL);
File f = new File(path);
if (f.exists()) {
log.debugf("add local template path = %s", f.getAbsolutePath());
FileTemplateResolver resolver = createFileTemplateResolver(f.getAbsolutePath());
resolver.setCacheable(false);
templateEngine.addTemplateResolver(resolver);
}
}
templateEngine.addTemplateResolver(createClassLoaderTemplateResolver());
log.debug("thymeleaf init complete");
}
public void addDialect(String prefix, String klassNames) {
for (String dialect : Strings.splitIgnoreBlank(klassNames)) {
log.debugf("loading thymeleaf dialect " + dialect);
Class<?> klass = Lang.loadClassQuite(dialect);
if (klass == null) {
log.info("no such thymeleaf dialect class = " + dialect);
continue;
}
Object obj = Mirror.me(klass).born();
if (obj instanceof IDialect) {
templateEngine.addDialect(prefix, (IDialect) obj);
}
}
}
@Override
public View make(Ioc ioc, String type, String value) {
if ("th".equalsIgnoreCase(type)) {
return new ThymeleafView(appContext.getClassLoader(), prop, value);
} else {
return null;
return new ThymeleafView(templateEngine, value, contentType, encoding);
}
return null;
}
@IocBean(name = "thymeleafTemplateEngine")
public TemplateEngine getTemplateEngine() {
return templateEngine;
}
}