java使用内嵌Tomcat开发javaWeb项目
写在前面
这一篇博客,是在java篇-(java使用内嵌Tomcat开发javaWeb项目-高级篇1)这篇博客之上进行扩展,完成高级篇之后的内容,集成shiro,文件上传服务,redis发布订阅,websocket,shiro session共享
java使用内嵌Tomcat开发javaWeb项目-高级篇1
这篇博客,博客地址:
java篇-(java使用内嵌Tomcat开发javaWeb项目-高级篇1
集成shiro
pom.xml添加shiro相关依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
在resources目录下面创建spring-shiro.xml
spring-shiro.xml用于存放shiro相关配置
创建shiroRealm,用于用户的授权认证
package com.lhstack.embed.realm;
import com.lhstack.embed.entity.domain.User;
import com.lhstack.embed.service.IUserService;
import org.apache.shiro.authc.*;
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 java.util.Optional;
/**
* shiro用于进行认真的realm
* @author lhstack
*/
public class UserRealm extends AuthorizingRealm {
private IUserService userService;
public void setUserService(IUserService userService) {
this.userService = userService;
}
@Override
protected void onInit() {
//这里不做密码匹配,通过在doGetAuthenticationInfo手动匹配
this.setCredentialsMatcher((authenticationToken, authenticationInfo) -> true);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
User primaryPrincipal = (User) principal.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//判断是否是lhstack这个账号,如果是,则赋予超级管理员权限
if("lhstack".equals(primaryPrincipal.getUsername())){
simpleAuthorizationInfo.addRole("ADMIN");
simpleAuthorizationInfo.addStringPermission("*:*");
}else{
simpleAuthorizationInfo.addRole("NORMAL");
simpleAuthorizationInfo.addStringPermission("normal:*");
}
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
//UsernamePasswordToken.getCredentials()调用的是getPassword()方法,返回的是char[]数组
String password = new String((char[]) authenticationToken.getCredentials());
Optional<User> optionalUser = this.userService.findByUsername(username);
if(optionalUser.isEmpty()){
throw new UnknownAccountException("用户不存在");
}
User user = optionalUser.get();
if(!user.getPassword().equals(password)){
throw new CredentialsException("用户名密码输入有误");
}
return new SimpleAuthenticationInfo(user,"",getName());
}
}
在IUserService和UserServiceImpl添加对应方法
IUserService.java
/**
* 根据用户名查询用户
* @param username
* @return
*/
Optional<User> findByUsername(String username);
UserServiceImpl.java
@Override
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
@Cacheable(cacheNames = "user",key = "'findByUsername username:::' + #p0")
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
在spring-shiro.xml添加shiro相关配置
spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="shiroFilterFactoryBean" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="webSecurityManager"/>
<property name="loginUrl" value="/login"/>
<property name="filterChainDefinitionMap">
<map>
<entry key="/login" value="anon"/>
<entry key="/static/**" value="anon"/>
<entry key="/**" value="authc"/>
</map>
</property>
</bean>
<!-- 开启权限认证切面 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="webSecurityManager"/>
</bean>
<!-- 会执行realm的init方法 -->
<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean id="userRealm" class="com.lhstack.embed.realm.UserRealm">
<property name="userService" ref="userServiceImpl"/>
</bean>
<aop:aspectj-autoproxy proxy-target-class="true" />
<bean id="webSecurityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="sessionManager" ref="sessionManager"/>
<property name="realm" ref="userRealm"/>
</bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 关闭session id url重写 -->
<property name="sessionIdUrlRewritingEnabled" value="false"/>
</bean>
</beans>
添加shiroFilterFactoryBean对应的filter代理
private static void addShiroFilter(Context context) {
DelegatingFilterProxy filterProxy = new DelegatingFilterProxy();
filterProxy.setTargetFilterLifecycle(true);
FilterDef filterDef = new FilterDef();
//这里与shiroFilterFactoryBean的id必须一致
filterDef.setFilterName("shiroFilterFactoryBean");
filterDef.setFilter(filterProxy);
//定义filter映射
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("shiroFilterFactoryBean");
filterMap.addURLPattern("/*");
//添加filter
context.addFilterDef(filterDef);
context.addFilterMap(filterMap);
}
创建HomeController,用来处理登陆逻辑
HomeController.java
package com.lhstack.embed.controller;
import com.lhstack.embed.entity.domain.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author lhstack
*/
@Controller
@RequestMapping
public class HomeController {
/**
* 转发到login.html页面
* @return
*/
@GetMapping("login")
public String toLogin(){
return "login";
}
/**
* 登陆逻辑
* @param user
* @return
*/
@PostMapping("login")
public String login(User user){
SecurityUtils.getSubject().login(new UsernamePasswordToken(user.getUsername(),user.getPassword()));
return "redirect:/thymeleaf/index";
}
}
创建登陆页面
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org/">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form th:action="${#request.contextPath} + '/login'" method="post">
<div>
<label for="username">用户名</label>
<input name="username" id="username" />
</div>
<div>
<label for="password">密码</label>
<input name="password" id="password" />
</div>
<div>
<input type="submit" value="登陆" /><input type="reset" value="重置" />
</div>
</form>
</body>
</html>
启动项目
通过浏览器访问 localhost:8080,然后没有登陆,就自动重定向到登陆页面
输入错误的用户,点击登陆
输入正确的用户和错误的密码,点击登陆
输入正确的用户名和密码,点击登陆
添加权限校验
删除之前创建的IndexController.kt(
不知道为什么,这里开启aop支持之后,kt里面注入的对象会出现空指针异常
)文件,创建IndexController.java
在IndexController添加对应的权限校验注解
IndexController.java
package com.lhstack.embed.controller;
import com.lhstack.embed.entity.domain.User;
import com.lhstack.embed.service.IUserService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Controller
@RequestMapping("thymeleaf")
public class IndexController {
@Autowired
private IUserService userService;
@GetMapping("index")
@RequiresPermissions("normal:list")
public String index(
Model model,
@RequestParam(name = "page", defaultValue = "1") Integer page,
@RequestParam(name = "size", defaultValue = "10") Integer size) {
model.addAttribute("list", this.userService.queryPageList(page, size));
return "index";
}
/**
* 跳转更新页面
*/
@GetMapping("toUpdatePage/{id}")
@RequiresPermissions("admin:toUpdate")
public String toUpdatePage(Model model, @PathVariable("id")Integer id) {
User user = this.userService.findById(id);
if (Objects.isNull(user)) {
return "redirect:/thymeleaf/index";
}
model.addAttribute("user", user);
return "update";
}
@PostMapping("update/{id}")
@RequiresPermissions("admin:update")
public String update(User user,@PathVariable("id")Integer id){
this.userService.update(id, user);
return "redirect:/thymeleaf/index";
}
/**
* 跳转保存页面
*/
@GetMapping("toSave")
@RequiresPermissions("admin:toSave")
public String toSavePage(){
return "save";
}
@PostMapping("save")
@RequiresPermissions("admin:save")
public String save(User user){
this.userService.save(user);
return "redirect:/thymeleaf/index";
}
@GetMapping("del/{id}")
@RequiresPermissions("admin:del")
public String delete(@PathVariable("id")Integer id){
this.userService.deleteById(id);
return "redirect:/thymeleaf/index";
}
}
启动项目,通过浏览器访问
登陆aaa账号
可以查看列表
点击新增,更新和删除
登陆lhstack账号
点击新增,添加用户
点击更新
点击删除
集成文件上传
pom.xml添加文件上传依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
在spring-mvc.xml中添加文件上传支持,并配置资源映射
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8" />
<property name="maxUploadSize" value="#{1024 * 1024 * 5}" />
<property name="maxUploadSizePerFile" value="#{1024 * 1024 * 5}" />
</bean>
<mvc:resources mapping="/resources/**" location="${user.dir}/files/" />
在HomeController里面添加文件上传接口
@Value("${user.dir}/files")
private String fileStorePath;
@PostMapping("upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
File director = new File(fileStorePath);
if(!director.exists()){
director.mkdirs();
}
file.transferTo(new File(director,file.getOriginalFilename()));
return "redirect:http://localhost:8080/resources/" + file.getOriginalFilename();
}
在sprign-shiro.xml中配置不需要拦截的接口
启动项目,使用postman测试
添加redis订阅功能
创建redis message listener
package com.lhstack.embed.listener;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
/**
* @author lhstack
*/
public class RedisMqMessageListener extends MessageListenerAdapter {
public RedisMqMessageListener() {
super.setDefaultListenerMethod("message");
}
public void message(String message){
System.out.println("收到redis mq message: " + message);
}
}
配置RedisMqMessageListener订阅redis消息
<bean id="redisMqMessageListener" class="com.lhstack.embed.listener.RedisMqMessageListener" />
<bean class="org.springframework.data.redis.listener.RedisMessageListenerContainer">
<property name="connectionFactory" ref="redisConnectionFactory"/>
<property name="messageListeners">
<map>
<entry key-ref="redisMqMessageListener">
<list>
<bean class="org.springframework.data.redis.listener.ChannelTopic">
<constructor-arg index="0" value="test"/>
</bean>
</list>
</entry>
</map>
</property>
</bean>
启动项目,并使用junit测试发布消息
消息成功发布并且收到了
修改redis默认序列化规则
<!-- 后续可能会用到调redis api的地方 ,这里先创建出redisTemplate -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="redisConnectionFactory"/>
<property name="enableDefaultSerializer" value="true"/>
<property name="defaultSerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
</bean>
重新启动项目,发布消息
可以看到,已经没有乱码了
集成websocket,实现与浏览器长连接通信
pom.xml添加websocket依赖
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.embed.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-websocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
创建websocket handler
package com.lhstack.embed.handler.ws;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
/**
* @author lhstack
*/
public class TestMessageHandler extends AbstractWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.printf("收到客户端: %s,message: %s%n",session.getRemoteAddress(),message.getPayload());
session.sendMessage(new TextMessage("我是服务端,我收到你的消息了"));
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.printf("client: %s,连接进来了%n",session.getRemoteAddress());
}
}
配置使用handler
spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:ws="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<!-- 配置validator -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>
<!-- 开启注解驱动,并使用validator -->
<mvc:annotation-driven validator="validator">
<mvc:message-converters>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="defaultCharset" value="UTF-8"/>
<property name="fastJsonConfig">
<bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
<property name="charset" value="UTF-8"/>
<property name="dateFormat" value="yyyy-MM-dd HH:mm:ss"/>
<property name="writeContentLength" value="true"/>
</bean>
</property>
<property name="supportedMediaTypes">
<array>
<value>application/json</value>
<value>application/json;charset=utf-8</value>
</array>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="testMessageHandler" class="com.lhstack.embed.handler.ws.TestMessageHandler"/>
<ws:handlers allowed-origins="*" allowed-origin-patterns="*">
<ws:mapping path="/ws/test" handler="testMessageHandler"/>
</ws:handlers>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8"/>
<property name="maxUploadSize" value="#{1024 * 1024 * 5}"/>
<property name="maxUploadSizePerFile" value="#{1024 * 1024 * 5}"/>
</bean>
<!-- 添加静态资源映射 -->
<mvc:resources mapping="/static/**" location="classpath:/static/"/>
<mvc:resources mapping="/resources/**" location="file:${user.dir}/files/"/>
<context:component-scan base-package="com.lhstack.embed.controller"/>
</beans>
在spring-shiro.xml中开放对应的地址
添加WsContextListener监听器到tomcat,用于初始化websocket环境
启动项目,通过websocket客户端连接
使用redis基于shiro实现session共享
session共享是分布式数据一致性的一种解决方案,当应用通过集群部署在不同机器上面 时,需要保证每台机器上的用户信息一致,这里就出现了一个分布式session的概念,通过中间件,将用户的session统一维护,而用户通过sessionId获取信息的时候,就直接去中间件里面获取,就能保证用户在请求的时候,不管是到集群里面那一台机器,都能正确的获取用户相关信息
pom.xml添加对应依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis.version}</version>
</dependency>
在spring-shiro.xml中配置session共享,缓存相关信息
spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="shiroFilterFactoryBean" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="webSecurityManager"/>
<property name="loginUrl" value="/login"/>
<property name="filterChainDefinitionMap">
<map>
<entry key="/login" value="anon"/>
<entry key="/resources/**" value="anon"/>
<entry key="/upload" value="anon"/>
<entry key="/static/**" value="anon"/>
<entry key="/ws/test" value="anon"/>
<entry key="/**" value="authc"/>
</map>
</property>
</bean>
<!-- 开启权限认证切面 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="webSecurityManager"/>
</bean>
<aop:aspectj-autoproxy proxy-target-class="false" expose-proxy="false"/>
<!-- 会执行realm的init方法 -->
<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean id="userRealm" class="com.lhstack.embed.realm.UserRealm">
<property name="userService" ref="userServiceImpl"/>
</bean>
<bean id="webSecurityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="sessionManager" ref="sessionManager"/>
<property name="realm" ref="userRealm"/>
<property name="cacheManager" ref="redisCacheManager" />
</bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 关闭session id url重写 -->
<property name="sessionIdUrlRewritingEnabled" value="false"/>
<property name="sessionDAO" ref="sessionDAO"/>
</bean>
<!-- redis相关,配置session共享,缓存 -->
<bean id="redisCacheManager" class="org.crazycake.shiro.RedisCacheManager">
<property name="redisManager" ref="redisManager"/>
<property name="keyPrefix" value="shiro-cache" />
</bean>
<bean id="sessionDAO" class="org.crazycake.shiro.RedisSessionDAO">
<property name="keyPrefix" value="shiro-session"/>
<property name="redisManager" ref="redisManager" />
</bean>
<bean id="redisManager" class="org.crazycake.shiro.RedisManager">
<property name="database" value="#{${redis.database} + 1}" />
<property name="host" value="${redis.host}:${redis.port}" />
</bean>
</beans>
启动项目,通过浏览器访问,并查看redis中是否存在缓存
访问页面,不登陆
session已经创建出来了
登陆
cache也进来了
过期时间也是有设置的,同时缓存的默认过期时间是1800s,可以在RedisCacheManager里面设置
session的过期时间跟随session过期时间而设置