Shiro介绍
Apache Shiro是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。
Apache Shiro的首要目标是易于使用和理解。安全通常很复杂,甚至让人感到很痛苦,但是Shiro却不是这样子的。一个好的安全框架应该屏蔽复杂性,向外暴露简单、直观的API,来简化开发人员实现应用程序安全所花费的时间和精力。
Shiro能做什么呢?
●验证用户身份
●用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
●在非 web 或 EJB 容器的环境下可以任意使用Session API
◆可以响应认证、访问控制,或者 Session 生命周期中发生的事件
◆可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
◆支持单点登录(SSO)功能
◆支持提供“Remember Me”服务,获取用户关联信息而无需登录
…
等等——都集成到一个有凝聚力的易于使用的API。
Shiro 致力在所有应用环境下实现上述功能,小到命令行应用程序,大到企业应用中,而且不需要借助第三方框架、容器、应用服务器等。当然 Shiro 的目的是尽量的融入到这样的应用环境中去,但也可以在它们之外的任何环境下开箱即用。
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:
●Authentication(认证):用户身份识别,通常被称为用户“登录”
●Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
●Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
●Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。特别是对以下的功能支持:
●Web支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。
●缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
●并发:Apache Shiro 支持多线程应用程序的并发特性。
●测试:支持单元测试和集成测试,确保代码和预想的一样安全。
●“Run As”:这个功能允许用户假设另一个用户的身份(在许可的前提下)。
●“Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。
项目介绍
1 springboot + shiro 数据是静态数据
2 在登陆界面输入用户名和密码,如果匹配成功,跳转到成功页面,否则将错误信息返回到当前页面,并且在登陆过程中用Shiro实现对用户的验证和授权
3 密码是 123456 123456 123456
项目下载
构建项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>SpringBoot_Shiro</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.7.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.7</java.version> </properties> <dependencies> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- aspectj的支持 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.11</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.12</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency> <!-- shiro spring. --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.2</version> </dependency> <!-- shiro ehcache --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency> <!-- 包含支持UI模版(Velocity,FreeMarker,JasperReports), 邮件服务, 脚本服务(JRuby), 缓存Cache(EHCache), 任务计划Scheduling(uartz)。 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <!--热部署.jar ,当你修改代码后,服务器会自动重启 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <!-- thmleaf模板依赖. --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> </dependencies> </project>
启动类
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 项目启动类 * @author Bean * */ @SpringBootApplication public class SpringBootShiroApplication { public static void main(String[] args) { SpringApplication.run(SpringBootShiroApplication.class, args); } }
Shiro配置类
package com.example.demo.configBean; import java.util.LinkedHashMap; /** * Shiro配置Bean */ import java.util.Map; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.RequestMapping; import com.example.demo.realms.MyShiroRealm; @Configuration public class ShiroConfigBean { @Bean public ShiroFilterFactoryBean shirFilter(DefaultWebSecurityManager securityManager) { System.out.println("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必须设置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 拦截器. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 设置login URL shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/LoginSuccess.action"); // 未授权的页面 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized.action"); // src="jquery/jquery-3.2.1.min.js" 生效 filterChainDefinitionMap.put("/jquery/*", "anon"); // 设置登录的URL为匿名访问,因为一开始没有用户验证 filterChainDefinitionMap.put("/login.action", "anon"); filterChainDefinitionMap.put("/Exception.class", "anon"); // 我写的url一般都是xxx.action,根据你的情况自己修改 filterChainDefinitionMap.put("/*.action", "authc"); // 退出系统的过滤器 filterChainDefinitionMap.put("/logout", "logout"); // 现在资源的角色 filterChainDefinitionMap.put("/admin.html", "roles[admin]"); // filterChainDefinitionMap.put("/user.html", "roles[user]"); // 最后一班都,固定格式 filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /* * 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 * 所以我们需要修改下doGetAuthenticationInfo中的代码; ) */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashIterations(1024);// 散列的次数,比如散列两次,相当于md5(md5("")); return hashedCredentialsMatcher; } @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 注入自定义的realm; securityManager.setRealm(myShiroRealm()); // 注入缓存管理器; securityManager.setCacheManager(ehCacheManager()); return securityManager; } /* * 开启shiro aop注解支持 使用代理方式;所以需要开启代码支持; */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。 */ @Bean @ConditionalOnMissingBean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator(); defaultAAP.setProxyTargetClass(true); return defaultAAP; } /* * shiro缓存管理器; * 需要注入对应的其它的实体类中-->安全管理器:securityManager可见securityManager是整个shiro的核心; */ @Bean public EhCacheManager ehCacheManager() { System.out.println("ShiroConfiguration.getEhCacheManager()"); EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml"); return cacheManager; } }
实体类
package com.example.demo.entity; /** * 实体类 * @author Bean * */ public class User { private Integer id;// 主键 private String username;// 用户名 private String password;// 密码 @Override public String toString() { return "User [id=" + id + ", username=" + username + ", password=" + password + "]"; } /** * @return the id */ public Integer getId() { return id; } /** * @param id * the id to set */ public void setId(Integer id) { this.id = id; } /** * @return the username */ public String getUsername() { return username; } /** * @param username * the username to set */ public void setUsername(String username) { this.username = username; } /** * @return the password */ public String getPassword() { return password; } /** * @param password * the password to set */ public void setPassword(String password) { this.password = password; } public User(String username, String password) { this.username = username; this.password = password; } public User() { } }
全局异常
package com.example.demo.exception; import java.util.Map; import org.apache.shiro.authz.UnauthorizedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * 全局异常捕捉类 * * @author Bean * */ @ControllerAdvice public class AllException { /// 角色權权限异常捕捉 @ExceptionHandler(value = UnauthorizedException.class) @ResponseBody // 在返回自定义相应类的情况下必须有,这是@ControllerAdvice注解的规定 public String roleException(UnauthorizedException e) { System.out.println("---------------------->" + e); return "角色权限不够!!!"; // return "/abc"; } // 其它异常异常捕捉 @ExceptionHandler(value = Exception.class) @ResponseBody // 在返回自定义相应类的情况下必须有,这是@ControllerAdvice注解的规定 public String allException(Exception e) { System.out.println("---------------------->" + e); return "系統出现异常!!!"; } }
controller
package com.example.demo.handler; import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class Ahandler { // 登录的url @RequestMapping({ "/login", "/" }) public String indexHtml() { return "/index"; } // 属于user角色@RequiresRoles("user") // 必须同时属于user和admin角@RequiresRoles({ "user", "admin" }) // 属于user或者admin之一;修改logical为OR 即可@RequiresRoles(value = { "user", "admin"}, // logical = Logical.OR) @RequestMapping("/showUserHtml.action") @RequiresRoles(value = { "user", "admin"},logical = Logical.OR) @RequiresPermissions("user:query") public String userHtml() { return "/user"; } @RequestMapping("/showAdminHtml.action") @RequiresRoles("admin") @RequiresPermissions("admin:query") public String adminHtml() { return "/admin"; } @RequestMapping("/unauthorized.action") public String unauthorized() { return "/abc"; } @RequestMapping("/LoginSuccess.action") public String listHtml() { return "/list"; } @RequestMapping("/error.action") public String error() { int a=1/0; return "/abc"; } }
LoginController
package com.example.demo.handler; import java.util.Map; import javax.servlet.http.HttpSession; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.ExpiredCredentialsException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.example.demo.service.UserService; /* * 登陆的controller */ @Controller public class LoginHandler { @Autowired private UserService userService; @RequestMapping("/login.action") public String login(String username, String password, Map<String, Object> map, HttpSession session) { System.out.println(username + "---" + password); // 获得当前Subject Subject currentUser = SecurityUtils.getSubject(); // 验证用户是否验证,即是否登录 if (!currentUser.isAuthenticated()) { String msg = ""; // 把用户名和密码封装为 UsernamePasswordToken 对象 UsernamePasswordToken token = new UsernamePasswordToken(username, password); // remembermMe记住密码 token.setRememberMe(true); try { // 执行登录. currentUser.login(token); // 登录成功... return "redirect:/LoginSuccess.action"; } catch (IncorrectCredentialsException e) { msg = "登录密码错误"; System.out.println("登录密码错误!!!" + e); } catch (ExcessiveAttemptsException e) { msg = "登录失败次数过多"; System.out.println("登录失败次数过多!!!" + e); } catch (LockedAccountException e) { msg = "帐号已被锁定"; System.out.println("帐号已被锁定!!!" + e); } catch (DisabledAccountException e) { msg = "帐号已被禁用"; System.out.println("帐号已被禁用!!!" + e); } catch (ExpiredCredentialsException e) { msg = "帐号已过期"; System.out.println("帐号已过期!!!" + e); } catch (UnknownAccountException e) { msg = "帐号不存在"; System.out.println("帐号不存在!!!" + e); } catch (UnauthorizedException e) { msg = "您没有得到相应的授权!"; System.out.println("您没有得到相应的授权!" + e); } catch (Exception e) { System.out.println("出错!!!" + e); } map.put("msg", msg); return "/index"; } // 登录成功,重定向到LoginSuccess.action return "redirect:/LoginSuccess.action"; } }
realm重点
user用户添加user角色,user:query权限
admin用户添加user,admin角色,user:query,admin:query权限
package com.example.demo.realms; import java.util.HashSet; import java.util.Set; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import com.example.demo.entity.User; import com.example.demo.service.UserService; /** * realm实现类,用于实现具体的验证和授权方法 * @author Bean * */ public class MyShiroRealm extends AuthorizingRealm { /** * 方面用于加密 参数:AuthenticationToken是从表单穿过来封装好的对象 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("doGetAuthenticationInfo:" + token); // 将AuthenticationToken强转为AuthenticationToken对象 UsernamePasswordToken upToken = (UsernamePasswordToken) token; // 获得从表单传过来的用户名 String username = upToken.getUsername(); // 从数据库查看是否存在用户 UserService userService = new UserService(); // 如果用户不存在,抛此异常 if (!userService.selectUsername(username)) { throw new UnknownAccountException("无此用户名!"); } // 认证的实体信息,可以是username,也可以是用户的实体类对象,这里用的用户名 Object principal = username; // 从数据库中查询的密码 Object credentials = userService.selectPassword(username); // 颜值加密的颜,可以用用户名 ByteSource credentialsSalt = ByteSource.Util.bytes(username); // 当前realm对象的名称,调用分类的getName() String realmName = this.getName(); // 创建SimpleAuthenticationInfo对象,并且把username和password等信息封装到里面 // 用户密码的比对是Shiro帮我们完成的 SimpleAuthenticationInfo info = null; info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName); return info; } // 用于授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("MyShiroRealm的doGetAuthorizationInfo授权方法执行"); // User user=(User) // principals.fromRealm(this.getClass().getName()).iterator().next();//获取session中的用户 // System.out.println("在MyShiroRealm中AuthorizationInfo(授权)方法中从session中获取的user对象:"+user); // 从PrincipalCollection中获得用户信息 Object principal = principals.getPrimaryPrincipal(); System.out.println("ShiroRealm AuthorizationInfo:" + principal.toString()); // 根据用户名来查询数据库赋予用户角色,权限(查数据库) Set<String> roles = new HashSet<>(); Set<String> permissions = new HashSet<>(); roles.add("user"); permissions.add("user:query"); if ("admin".equals(principal)) { roles.add("admin"); permissions.add("admin:query"); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); //天加权限 info.setStringPermissions(permissions); return info; // return null; } }
ehcache(不需要修改,直接用)
<!-- ~ Hibernate, Relational Persistence for Idiomatic Java ~ ~ License: GNU Lesser General Public License (LGPL), version 2.1 or later. ~ See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. --> <ehcache> <!-- Sets the path to the directory where cache .data files are created. If the path is a Java System Property it is replaced by its value in the running VM. The following properties are translated: user.home - User's home directory user.dir - User's current working directory java.io.tmpdir - Default temp file path --> <diskStore path="java.io.tmpdir"/> <!--Default Cache configuration. These will applied to caches programmatically created through the CacheManager. The following attributes are required for defaultCache: maxInMemory - Sets the maximum number of objects that will be created in memory eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the element is never expired. timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used if the element is not eternal. Idle time is now - last accessed time timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used if the element is not eternal. TTL is now - creation time overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache has reached the maxInMemory limit. --> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" /> <!--Predefined caches. Add your cache configuration settings here. If you do not have a configuration for your cache a WARNING will be issued when the CacheManager starts The following attributes are required for defaultCache: name - Sets the name of the cache. This is used to identify the cache. It must be unique. maxInMemory - Sets the maximum number of objects that will be created in memory eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the element is never expired. timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used if the element is not eternal. Idle time is now - last accessed time timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used if the element is not eternal. TTL is now - creation time overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache has reached the maxInMemory limit. --> <!-- Sample cache named sampleCache1 This cache contains a maximum in memory of 10000 elements, and will expire an element if it is idle for more than 5 minutes and lives for more than 10 minutes. If there are more than 10000 elements it will overflow to the disk cache, which in this configuration will go to wherever java.io.tmp is defined on your system. On a standard Linux system this will be /tmp" --> <cache name="sampleCache1" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true" /> <!-- Sample cache named sampleCache2 This cache contains 1000 elements. Elements will always be held in memory. They are not expired. --> <cache name="sampleCache2" maxElementsInMemory="1000" eternal="true" timeToIdleSeconds="0" timeToLiveSeconds="0" overflowToDisk="false" /> --> <!-- Place configuration for your caches following --> </ehcache>
UserServcie
package com.example.demo.service; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Service; import com.example.demo.entity.User; /** * User的service,这里面的数据时静态数据,不查询数据库了 * @author lenovo * */ @Service public class UserService { // 用户的集合 private List<User> users = new ArrayList<>(); public UserService() { // 从数据库查出来的用户名,密码,这是是静态数据(密码是123456) users.add(new User("admin", "038bdaf98f2037b31f1e75b5b4c9b26e")); users.add(new User("user", "098d2c478e9c11555ce2823231e02ec1")); } // 判断是否用户名是否存在 public boolean selectUsername(String username) { for (User user : users) { if (user.getUsername().equals(username)) { return true; } } return false; } // 根据用户返回查询的密码 public String selectPassword(String username) { for (User user : users) { if (user.getUsername().equals(username)) { return user.getPassword(); } } return ""; } }
abc.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"></meta> <title>Insert title here</title> </head> <body> <h1>abc HTML</h1> <h4 th:text="${msg}"></h4> </body> </html>
admin.html user.html差不多
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"></meta> <title>Insert title here</title> </head> <body> <h1>user HTML</h1> </body> </html>
index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"></meta> <title>Insert title here</title> <script type="text/javascript" src="jquery/jquery-3.2.1.min.js"></script> <script th:inline="javascript"> /*<![CDATA[*/ $(function(){ var msg = [[${msg}]]; if(msg!="null"&&msg!=null&&msg!=""){ alert(msg); } }); /*]]>*/ </script> </head> <body> <h1>Login HTML</h1> <h4 th:text="${msg}"></h4> <form action="login.action" method="post"> 用户名:<input type="text" name="username" /><br /> 密码:<input type="password" name="password" /><br /> <input type="submit" value="Submit" /> </form> </body> </html>
list.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"></meta> <title>Insert title here</title> </head> <body> <h1>list HTML</h1> <a href="showUserHtml.action">user html</a> <a href="showAdminHtml.action">admin html</a> <a href="error.action">error 1/0 html</a> <a href="logout">退出</a> </body> </html>