ssm的web项目整合就不过多哔哔了,直接整合 Spring Security,接下来将通过xml的方式和Java 代码配置方式来实现
本篇博文使用的Spring Security版本为:4.2.4.RELEASE
目录
环境准备:
sql:
-- ----------------------------
-- Table structure for t_sec_member
-- ----------------------------
DROP TABLE IF EXISTS `t_sec_member`;
CREATE TABLE "t_sec_member" (
"id" int(11) NOT NULL AUTO_INCREMENT COMMENT '用户表id',
"username" varchar(50) NOT NULL COMMENT '用户名',
"password" varchar(100) NOT NULL COMMENT '用户密码,加密',
"email" varchar(50) DEFAULT NULL,
"phone" varchar(20) DEFAULT NULL,
"question" varchar(100) DEFAULT NULL COMMENT '找回密码问题',
"answer" varchar(100) DEFAULT NULL COMMENT '找回密码答案',
"status" tinyint(1) NOT NULL COMMENT '账户的状态',
"status_str" varchar(255) DEFAULT NULL COMMENT '账户的状态描述',
"create_time" datetime NOT NULL COMMENT '创建时间',
"update_time" datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次更新时间',
PRIMARY KEY ("id"),
UNIQUE KEY "user_name_unique" ("username") USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_sec_member
-- ----------------------------
INSERT INTO `t_sec_member` VALUES ('1', 'admin', '$2a$10$qh7Bia2mC8lw5MyrtPz.YOsxOL99N8N2Z8qEm0skWsBwSNof90Z/2', 'admin@happymmall.com', '13800138000', '问题', '答案', '1', null, '2016-11-06 16:56:45', '2019-06-08 15:46:08');
INSERT INTO `t_sec_member` VALUES ('2', 'sxh', '$2a$10$qh7Bia2mC8lw5MyrtPz.YOsxOL99N8N2Z8qEm0skWsBwSNof90Z/2', '123@456.com', '123456789', 'wu', 'wu', '1', null, '2019-03-31 12:58:40', '2019-06-08 15:46:09');
INSERT INTO `t_sec_member` VALUES ('3', 'test', '$2a$10$qh7Bia2mC8lw5MyrtPz.YOsxOL99N8N2Z8qEm0skWsBwSNof90Z/2', '123@456.com', '1383838383838', null, null, '1', 'test账户', '2019-06-09 22:01:47', '2019-06-09 22:01:52');
INSERT INTO `t_sec_member` VALUES ('6', 'dba', '$2a$10$qh7Bia2mC8lw5MyrtPz.YOsxOL99N8N2Z8qEm0skWsBwSNof90Z/2', 'null', '123838383838', null, null, '1', '账户可用', '2019-06-11 14:54:08', '2019-06-11 14:55:29');
-- ----------------------------
-- Table structure for t_sec_member_role
-- ----------------------------
DROP TABLE IF EXISTS `t_sec_member_role`;
CREATE TABLE "t_sec_member_role" (
"member_id" int(11) NOT NULL,
"role_id" int(11) NOT NULL,
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
KEY "k_role_id" ("role_id")
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_sec_member_role
-- ----------------------------
INSERT INTO `t_sec_member_role` VALUES ('1', '1', '2019-06-11 13:58:08', '2019-06-11 13:58:08');
INSERT INTO `t_sec_member_role` VALUES ('2', '2', '2019-06-11 13:58:08', '2019-06-11 13:58:08');
INSERT INTO `t_sec_member_role` VALUES ('3', '1', '2019-06-11 14:03:43', '2019-06-11 14:03:43');
INSERT INTO `t_sec_member_role` VALUES ('3', '2', '2019-06-11 14:03:50', '2019-06-11 14:03:50');
INSERT INTO `t_sec_member_role` VALUES ('3', '3', '2019-06-11 14:17:11', '2019-06-11 14:17:11');
INSERT INTO `t_sec_member_role` VALUES ('6', '3', '2019-06-11 14:55:03', '2019-06-11 14:55:59');
-- ----------------------------
-- Table structure for t_sec_role
-- ----------------------------
DROP TABLE IF EXISTS `t_sec_role`;
CREATE TABLE "t_sec_role" (
"id" int(11) NOT NULL,
"role_name" varchar(50) NOT NULL COMMENT '角色名称',
"role_desc" varchar(50) DEFAULT NULL COMMENT '角色描述',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY ("id")
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_sec_role
-- ----------------------------
INSERT INTO `t_sec_role` VALUES ('1', 'ROLE_ADMIN', '超级管理员角色', '2019-06-11 13:59:53', '2019-06-11 15:24:50');
INSERT INTO `t_sec_role` VALUES ('2', 'ROLE_MANAGER', '普通管理员角色', '2019-06-11 13:59:53', '2019-06-11 15:24:53');
INSERT INTO `t_sec_role` VALUES ('3', 'ROLE_DBA', '数据库管理员', '2019-06-11 13:59:53', '2019-06-11 15:24:55');
INSERT INTO `t_sec_role` VALUES ('4', 'ROLE_ORDINARY', '普通账户角色', '2019-06-11 14:07:22', '2019-06-11 15:24:59');
INSERT INTO `t_sec_role` VALUES ('5', 'ROLE_TEMP', '临时的账户角色', '2019-06-11 14:11:16', '2019-06-11 15:25:03');
# 根据username来查询对应的权限:
SELECT
r.id,
r.role_name,
r.role_desc,
r.create_time,
r.update_time
FROM
t_sec_role AS r
LEFT JOIN t_sec_member AS m ON m.username = 'admin'
INNER JOIN `t_sec_member_role` AS mr ON m.id = mr.member_id
AND mr.role_id = r.id;
Spring Security的依赖,maven座标:
<properties>
<!-- spring版本号 -->
<spring.version>4.3.3.RELEASE</spring.version>
<security.version>4.2.4.RELEASE</security.version>
<!-- log4j日志文件管理包版本 -->
<slf4j.version>1.7.7</slf4j.version>
<log4j.version>1.2.17</log4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>${security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${security.version}</version>
</dependency>
</dependencies>
登录的主要逻辑:
@Service
public class MemberServiceImpl implements IMemberService {
@Autowired(required = true)
private MemberMapper memberMapper;
@Autowired(required = true)
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(StringUtils.isEmpty(username)) {
throw new BadCredentialsException("用户名不能为空");
}
Member member = memberMapper.findByUserName(username);
if (member == null) {
throw new BadCredentialsException("用户名不存在");
}
return new User(username, member.getPassword(), getAuthorities(username));
}
private ArrayList<? extends GrantedAuthority> getAuthorities(String username) {
List<Role> roles = roleMapper.selectByUsername(username);
ArrayList<SimpleGrantedAuthority> list = new ArrayList();
for (Role item : roles) {
list.add(new SimpleGrantedAuthority(item.getRoleName()));
}
return list;
}
}
配置:
以下的两种配置方式都没有将使用 Java 类的配置方式来替换 web.xml,仍保留了web.xml。
1.使用xml方式:
web.xml中必须配置:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
security的配置,这个配置需要被加到容器中,spring-security.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="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-4.3.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.2.xsd">
<!--将这些资源路径设置为不被拦截-->
<http security="none" pattern="/html/login.html"/>
<http security="none" pattern="/html/failure.html"/>
<http security="none" pattern="/css/**"/>
<http security="none" pattern="/plugin/**"/>
<http security="none" pattern="/error/**"/>
<!--页面的拦截规则
use-expressions:是否使用SPEL表达式,默认是true
-->
<http auto-config="true" use-expressions="false">
<!--路径'/admin/*'需要权限ROLE_ADMIN-->
<!--<security:intercept-url pattern="/admin/**" access="ROLE_ADMIN"/>-->
<!--以"/user"开头的所有路径需要ROLE_USER权限-->
<!--<security:intercept-url pattern="/user/**" access="ROLE_USER"/>-->
<!--配置需要拦截的url,pattern:拦截路径,access:允许访问的权限-->
<intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN"/>
<!--
login-page="/login.html":配置自定义的登录页面login.html
login-processing-url="/login":表示登录时提交的地址为"/login"
username-parameter:表示登录时用户名使用的是哪个参数
password-parameter:表示登录时密码使用的是哪个参数
default-target-url:
默认情况下,在登录成功后会返回到原本受限制的页面
如果用户是直接请求登录页面,登录成功后默认情况下会跳转到当前应用的根路径,即欢迎页面
default-target-url 属性可以指定,用户直接访问登录页面并登陆成功后跳转的页面
如果想让用户不管是直接请求登录页面,还是通过 Spring Security 引导过来的,登录之后都跳转到指定的页面,可以使用 always-use-default-target 属性为 true 来达到这一效果
authentication-success-handler-ref:
对应一个 AuthencticationSuccessHandler 实现类的引用
登录认证成功后会调用指定 AuthenticationSuccessHandler 的 onAuthenticationSuccess 方法,在此方法中进行登陆成功后的处理
此时 default-target-url 失效
authentication-failure-url:
指定登录认证失败后跳转的页面
默认情况下登录失败后会返回登录页面
登录失败后跳转的页面,也需放行,否则又会被重定向到登录页面。
authentication-failure-handler-ref:
对应一个用于处理认证失败的 AuthenticationFailureHandler 实现类。
指定了该属性,Spring Security 在认证失败后会调用指定 AuthenticationFailureHandler 的 onAuthenticationFailure 方法对认证失败进行处理
此时 authentication-failure-url 属性将不再发生作用。
authentication-success-forward-url="/success.html":登录认证成功后转发的页面success.html
authentication-failure-forward-url="/failure.html":登录认证失败后的转发页面为failure.html
配置了转发后就不会走这个默认的url
-->
<form-login login-page="/html/login.html"
login-processing-url="/login"
default-target-url="/html/success.html"
username-parameter="username"
password-parameter="password"
authentication-failure-forward-url="/html/failure.html"
authentication-success-forward-url="/html/success.html"
/>
<!--指定使用默认登出页面,登出后跳转到/login?logout页面-->
<!--<logout logout-success-url="/login?logout"/>-->
<!--指定登陆认证成功后,对于没有权限的页面跳转到/403路径-->
<!--<access-denied-handler error-page="/error/403"/>-->
<!--会话管理-->
<session-management session-fixation-protection="none">
<!--
max-sessions="1":同一用户只能在一个浏览器登录,当尝试在其他浏览器登陆时将被拒绝
error-if-maximum-exceeded="true":当设置了此属性,尝试在其他浏览器登录时,则原会话将被终止,将在新窗口建立新会话
-->
<concurrency-control max-sessions="1"/>
</session-management>
<!--关闭csrf跨域-->
<csrf disabled="true"/>
<!--注销登录-->
<!--<logout invalidate-session="true" logout-success-url="/login.html" logout-url="/logout"/>-->
</http>
<!--认证管理器-->
<authentication-manager>
<authentication-provider user-service-ref="userService">
<password-encoder ref="passwordEncoder"/>
<!--
<user-service>
<user name="admin" password="admin" authorities="ROLE_USER,ROLE_ADMIN"/>
<user name="sxh" password="passwd" authorities="ROLE_USER"/>
</user-service>
-->
</authentication-provider>
</authentication-manager>
<beans:bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<beans:bean id="userService" class="com.sxh.study.service.impl.MemberServiceImpl" >
<intercept-methods>
<protect access="ROLE_USER" method="find*"/>
<protect access="ROLE_USER" method="query*"/>
<protect access="ROLE_ADMIN" method="add*"/>
<protect access="ROLE_ADMIN" method="update*"/>
<protect access="ROLE_ADMIN" method="delete*"/>
</intercept-methods>
</beans:bean>
</beans:beans>
2.使用java类来配置:
若你的web.xml中配置了springSecurityFilterChain,则需要删除或者注释。
否则会抛出异常:Caused by: java.lang.IllegalStateException: Duplicate Filter registration for 'springSecurityFilterChain'.
<--
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
-->
- 写一个类来继承 WebSecurityConfigurerAdapter,此类就代替了spring-security.xml的配置。同样spring-security.xml配置文件不能被扫进容器中,
否则会抛出异常:Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'org.springframework.security.authenticationManager': Requested bean is currently in creation: Is there an unresolvable circular reference?
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private IMemberService memberService;
@Override
public void configure(WebSecurity web) throws Exception {
// 设置不拦截规则
web.ignoring().antMatchers("/error/**",
"/css/**",
"/help/**",
"/img/**",
"/js/**",
"/html/login.html",
"/html/failure.html",
"/res/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* authorizeRequests() 通过 ExpressionUrlAuthorizationConfigurer 去配置基于 URL 请求地址的权限控制,它支持权限表达式,默认是UrlAuthorizationConfigurer
* formLogin() 对应表单认证相关的配置
* logout() 对应了注销相关的配置
* httpBasic() 可以配置basic登录
* etc
* sessionManagement() session管理
*/
http
.authorizeRequests()
.antMatchers("/resources/**", "/html/login.html", "/html/failure.html").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
/*
anyRequest:任何的请求
authenticated:认证
*/
.anyRequest().authenticated()
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.successForwardUrl("/html/success.html")
.failureForwardUrl("/html/failure.html")
.loginPage("/html/login.html")
.loginProcessingUrl("/login")
.permitAll()
.and()
/*
// 定义登录认证失败后执行的操作
.failureHandler(this.authenticationFailureHandler())
// 定义登录认证曾工后执行的操作
.successHandler(this.authenticationSuccessHandler());
*/
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/html/login.html")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
.and()
.httpBasic()
.disable()
// 禁用 CSRF
.csrf()
.disable()
// session管理
.sessionManagement()
.sessionFixation().none().maximumSessions(1);
}
/**
* 登录认证配置
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberService)
.passwordEncoder(this.bCryptPasswordEncoder());
}
@Bean(name = "bCryptPasswordEncoder")
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 需要一个类来继承AbstractSecurityWebApplicationInitializer,否则此过滤器是不会起作用的。
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
/*
spring或者springmvc中会自动将此类注入到父类中
若当前环境没有使用Spring或SpringMVC,则需要将WebSecurityConfig(SpringSecurity配置类)传入超类,以确保获取配置,并创建springcontext
public SecurityWebApplicationInitializer() {
super(WebSecurityConfig.class);
}
*/
}
- 需要将security的配置类加到spring容器中,不然会抛出找不到springSecurityFilterChain这个bean的定义异常
如果你使用Java类配置的方式替换了 web.xml,则需要修改 getRootConfigClasses() 方法,添加 WebSecurityConfig.class:
@Override
protectedClass<?>[]getRootConfigClasses(){
return newClass<?>[]{ ApplicationConfig.class, WebSecurityConfig.class};
}
否则就用其他你所知道的方式将配置类 load 到 Spring 容器中,如:通过组件扫描:
<context:component-scan base-package="a.b.c.package" />