spring篇-(ssm整合shiro)
ssm整合shiro
- 初始化项目结构
- 启动项目
初始化项目结构
项目基于maven的ssm架构,最终打包的结果为war
结构如下图
项目pom文件
<?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.lhstack</groupId>
<artifactId>shiro-spring-web</artifactId>
<version>0.0.1</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring.version>5.3.5</spring.version>
<shiro.version>1.7.1</shiro.version>
<spring-security.version>5.4.5</spring-security.version>
<jackson.version>2.12.2</jackson.version>
</properties>
<dependencies>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- spring整合 junit -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- json序列化工具相关依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- 加密工具依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>${spring-security.version}</version>
</dependency>
<!-- shiro整合spring的相关依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring上下文初始依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- aop相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.0</version>
</plugin>
</plugins>
</build>
</project>
log4j2配置
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp" packages="">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<!-- info-->
<RollingRandomAccessFile name="InfoAppendLogger" fileName="logs/info/info.log" filePattern="logs/info/$${date:yyyy-MM-dd}/info-%d{yyyy-MM-dd}-%i.log">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
<!-- 过滤日志级别 -->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1"/>
<SizeBasedTriggeringPolicy size="50 MB"/>
</Policies>
<!-- 清理过期日志 -->
<DefaultRolloverStrategy>
<Delete basePath="logs/info/" maxDepth="2">
<IfFileName glob="*/*.log" />
<IfLastModified age="7d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>
<Async name="AsyncInfoLoggerAppender">
<AppenderRef ref="InfoAppendLogger"/>
</Async>
<!-- error-->
<RollingRandomAccessFile name="ErrorAppendLogger" fileName="logs/error/error.log" filePattern="logs/error/$${date:yyyy-MM-dd}/error-%d{yyyy-MM-dd}-%i.log">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1"/>
<SizeBasedTriggeringPolicy size="50 MB"/>
</Policies>
<DefaultRolloverStrategy>
<Delete basePath="logs/error/" maxDepth="2">
<IfFileName glob="*/*.log" />
<IfLastModified age="7d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>
<Async name="AsyncErrorLoggerAppender">
<AppenderRef ref="ErrorAppendLogger"/>
</Async>
<!-- warn-->
<RollingRandomAccessFile name="WarnAppendLogger" fileName="logs/warn/warn.log" filePattern="logs/warn/$${date:yyyy-MM-dd}/warn-%d{yyyy-MM-dd}-%i.log">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1"/>
<SizeBasedTriggeringPolicy size="50 MB"/>
</Policies>
<DefaultRolloverStrategy>
<Delete basePath="logs/warn/" maxDepth="2">
<IfFileName glob="*/*.log" />
<IfLastModified age="7d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>
<Async name="AsyncWarnLoggerAppender">
<AppenderRef ref="WarnAppendLogger"/>
</Async>
<!-- debug-->
<RollingRandomAccessFile name="DebugAppendLogger" fileName="logs/debug/debug.log" filePattern="logs/debug/$${date:yyyy-MM-dd}/debug-%d{yyyy-MM-dd}-%i.log">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1"/>
<SizeBasedTriggeringPolicy size="50 MB"/>
</Policies>
<DefaultRolloverStrategy>
<Delete basePath="logs/debug/" maxDepth="2">
<IfFileName glob="*/*.log" />
<IfLastModified age="7d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>
<Async name="AsyncDebugLoggerAppender">
<AppenderRef ref="DebugAppendLogger"/>
</Async>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="AsyncInfoLoggerAppender"/>
<AppenderRef ref="AsyncErrorLoggerAppender"/>
<AppenderRef ref="AsyncWarnLoggerAppender"/>
<AppenderRef ref="AsyncDebugLoggerAppender"/>
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
<!-- 拦截所有请求,让所有请求都走DispatcherServlet -->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 字符编码处理 -->
<filter>
<filter-name>CharacterFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<!--和bean里面的shiroFilterFactoryBean名称必须相同,不然不会生效,代理与filter同名的bean,让被代理的filter拥有springbean的功能-->
<filter-name>shiroFilterFactoryBean</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilterFactoryBean</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<!-- 启动监听器,加载webmvc以外的配置文件,用于配置容器隔离 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application-*.xml</param-value>
</context-param>
</web-app>
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" 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">
<!-- 开启mvc注解驱动,同WebMvcConfigurer -->
<mvc:annotation-driven>
<mvc:message-converters>
<!-- 配置json序列化转换器 -->
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="defaultCharset" value="UTF-8" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- controller包扫描工具,用于扫描controller接口,spring-mvc需要配置此功能 -->
<context:component-scan base-package="com.lhstack.spring.shiro.controller">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
shiro相关配置
application-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">
<!-- shiro核心过滤器,所有的功能都来自于这个类 -->
<bean name="shiroFilterFactoryBean" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="webSecurityManager" />
<!-- 如果在filterChainDefinitionMap配置了如 key=login value=login,那登录成功之后会回调这个地址 -->
<property name="successUrl" value="/" />
<!-- 未登录重定向的地址 -->
<property name="loginUrl" value="/login.jsp" />
<!-- 登录成功,但是访问地址未授权,重定向地址 -->
<property name="unauthorizedUrl" value="/405.jsp" />
<!-- 配置路由规则,key为路由,value为规则 -->
<!-- anon 不做任何处理,logout 退出登录 login 登录 authc 需要授权 其他过滤器功能可以在org.apache.shiro.web.filter.mgt.DefaultFilter类中查看 -->
<property name="filterChainDefinitionMap">
<map>
<entry key="/login.jsp" value="anon" />
<entry key="/login" value="anon" />
<entry key="/session" value="anon" />
<entry key="/**" value="authc" />
</map>
</property>
</bean>
<!-- 密码匹配器,自定义 -->
<bean name="bcryPasswordMatcher" class="com.lhstack.spring.shiro.matcher.BcryPasswordMatcher" >
<property name="passwordEncoder" ref="bCryptPasswordEncoder" />
</bean>
<!-- realm,需要自己实现 -->
<bean name="testRealm" class="com.lhstack.spring.shiro.realm.TestRealm">
<property name="passwordEncoder" ref="bCryptPasswordEncoder" />
<!-- 注入密码匹配器 -->
<property name="credentialsMatcher" ref="bcryPasswordMatcher" />
</bean>
<!-- webSecurityManager,shiroFilterFactoryBean需要此类,用于管理授权认证的操作 -->
<bean name="webSecurityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" >
<property name="realm" ref="testRealm" />
<property name="sessionManager" ref="webSessionManager" />
</bean>
<!-- 自定义session管理器,用来持久session,可扩展实现分布式session -->
<bean name="localSessionDao" class="com.lhstack.spring.shiro.session.LocalSessionDao" />
<!-- 初始化realm,会回调realm的onInit方法-->
<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<!-- session管理器,管理session -->
<bean name="webSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 取消sessionid重写功能,防止页面报错 -->
<property name="sessionIdUrlRewritingEnabled" value="false"/>
<property name="sessionDAO" ref="localSessionDao" />
</bean>
<!-- 密码编码匹配工具 -->
<bean name="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
<!-- 开启aop支持 注解鉴权需要aop功能支持 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
<!-- 开启shiro注解权限的功能 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor" />
</beans>
TestRealm.java
package com.lhstack.spring.shiro.realm;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Collections;
public class TestRealm extends AuthorizingRealm {
//模拟盐值的功能
public static final String SALT = "asxkdhz1568462416575";
//密码工具
private PasswordEncoder passwordEncoder;
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return new SimpleAuthorizationInfo(Collections.singleton("ADMIN"));
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
char[] passwordArray = (char[]) authenticationToken.getCredentials();
String password = new String(passwordArray);
//用户输入的密码,然后进行盐值处理加编码
String encoder = passwordEncoder.encode(password + SALT);
//返回用户的信息,然后将处理过后的用户输入的密码设置进去
return new
SimpleAuthenticationInfo(username,encoder,this.getName());
}
}
BcryPasswordMatcher.java
package com.lhstack.spring.shiro.matcher;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public class BcryPasswordMatcher implements CredentialsMatcher {
private PasswordEncoder passwordEncoder;
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
/** * 这是simple Matcher的源码,是equals的,比较传入的密码和realm返回的对象的密码,一般进行编码之后,就不一样了,所以需要自定义实现matcher * public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { * Object tokenCredentials = this.getCredentials(token); * Object accountCredentials = this.getCredentials(info); * return this.equals(tokenCredentials, accountCredentials); * } * 正常操作,密码比较应该用这个类来实现,如果在realm里面能完成匹配,则这里默认返回true就行了 * @param authenticationToken * @param authenticationInfo * @return */
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
//获取刚刚realm返回的处理过后的用户输入的密码
String password = (String) authenticationInfo.getCredentials();
//这里应该是数据库的密码,由authenticationInfo.authenticationInfo.getPrincipals().getPrimaryPrincipal()获取,这个对象的信息由Realm返回的SimpleAuthenticationInfo里面的第一个参数获得,如new SimpleAuthenticationInfo(userInfo,password,Realm.this.getName());获取到的就是userInfo,里面对应应存在用户在数据库的密码和盐值,然后matches第一个参数就是没有编码过后的用户密码和盐值处理过后的值
return passwordEncoder.matches("123456asxkdhz1568462416575",password);
}
}
LocalSessionDao.java
验证session存储功能
package com.lhstack.spring.shiro.session;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SimpleSession;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/** * @author lhstack */
public class LocalSessionDao extends AbstractSessionDAO {
private final Map<Serializable,Session> map = new HashMap<>();
@Override
protected Serializable doCreate(Session session) {
Serializable serializable = this.generateSessionId(session);
if(session instanceof SimpleSession){
((SimpleSession) session).setId(serializable);
}
map.put(serializable,session);
return serializable;
}
@Override
protected Session doReadSession(Serializable serializable) {
return map.get(serializable);
}
@Override
public void update(Session session) throws UnknownSessionException {
map.put(session.getId(),session);
}
@Override
public void delete(Session session) {
map.remove(session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
return map.values();
}
}
TestController.java
package com.lhstack.spring.shiro.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Map;
/** * @author lhstack */
@RequestMapping
@Controller
public class TestController {
@Autowired
private SessionDAO sessionDAO;
@GetMapping(value = "session")
@ResponseBody
public Object hello(){
return sessionDAO.getActiveSessions();
}
@GetMapping("index")
public String index(){
return "index";
}
@PostMapping("login")
public String login(@RequestParam(name = "username") String username,
@RequestParam(name = "password") String password) throws IOException {
SecurityUtils.getSubject().login(new UsernamePasswordToken(username,password));
return "redirect:index";
}
}
启动项目
我这里使用的是本地tomcat启动
访问页面,如localhost:8080/xxx,会自动跳转到登录页面
输入密码admin 密码1234567,会弹出异常页面,这里因为没有配置异常处理器,所以会使用springmvc默认的异常处理器,会打印出异常信息
意思就是密码匹配异常
输入正确的用户名和密码
就会跳转登录成功的页面,因为这里没有使用shiro的login,由通过Subject.login()实现,所以会按照controller里面的逻辑实现跳转
还没有评论,来说两句吧...