java篇-(java使用内嵌Tomcat开发javaWeb项目-高级篇2)

  • Post author:
  • Post category:java




写在前面


这一篇博客,是在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过期时间而设置

在这里插入图片描述



版权声明:本文为qq_42413011原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。