一、SpringSecurity
1.1 什么是SpringSecurity
Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。(官网地址)
Spring Security 为基于J2EE企业应用软件提供了全面安全服务。特别是使用领先的J2EE解决方案-Spring框架开发的企业软件项目。人们使用Spring Security有很多种原因,不过通常吸引他们的是在J2EE Servlet规范或EJB规范中找不到典型企业应用场景的解决方案。特别要指出的是他们不能再
WAR 或 EAR 级别进行移植。这样,如果你更换服务器环境,就要,在新的目标环境进行大量的工作,对你的应用
系统进行重新配置安全。使用Spring Security 解决了这些问题,也为你提供很多有用的,完全可以指定的其他安
全特性。安全包括两个主要操作。
- “认证”,是为用户建立一个他所声明的主体。主体一般是指用户,设备或可以在你系统中执行动作的其他系
统。(可以将主体当前权限框架自己的session,认证其实就是登录操作,并将登录成功的数据信息存入主体)
- “授权”,指的是一个用户能否在你的应用中执行某个操作,在到达授权判断之前,身份的主题已经由身份验证
过程建立了。(查询是否对应权限,授权其实就是在认证之后请求需要权限的资源时,查询数据库在主体中保存对应权限数据)
这些概念是通用的,不是Spring Security特有的。在身份验证层面,Spring Security广泛支持各种身份验证模式,
这些验证模型绝大多数都由第三方提供,或则正在开发的有关标准机构提供的,例如 Internet Engineering Task
Force.作为补充,Spring Security 也提供了自己的一套验证功能。
Spring Security 目前支持认证一体化如下认证技术:
HTTP BASIC authentication headers (一个基于IEFT RFC 的标准)
HTTP Digest authentication headers (一个基于IEFT RFC 的标准)
HTTP X.509 client certi?cate exchange(一个基于IEFT RFC 的标准)
LDAP (一个非常常见的跨平台认证需要做法,特别是在大环境)
Form-based-authentication (提供简单用户接口的需求)
OpenID authentication Computer Associates Siteminder JA-SIG Central Authentication Service (CAS,这是一个流行的开源单点登录系统)
Transparent authentication context propagation for Remote Method Invocation and HttpInvoker (一个Spring远程调用协议)
1.2 核心组件
SecurityContextHolder
SecurityContextHolder它持有的是安全上下文 (security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权等等,这些都被保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到 ThreadLocal 也就意味着,这是一种与线程绑定的策略。在web环境下,Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息
看源码他有静态方法
//获取 上下文
public static SecurityContext getContext() {
return strategy.getContext();
}
//清除上下文
public static void clearContext() {
strategy.clearContext();
}
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
getAuthentication() 返回了认证信息,
getPrincipal() 返回了身份信息
UserDetails 便是Spring对身份信息封装的一个接口
SecurityContext
安全上下文,主要持有 Authentication 对象,如果用户未鉴权,那Authentication对象将会是空的。看源码可知
package org.springframework.security.core.context;
import java.io.Serializable;
import org.springframework.security.core.Authentication;
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication var1);
}
Authentication
鉴权对象,该对象主要包含了用户的详细信息 (UserDetails) 和用户鉴权时所需要的信息,如用户提交的用户名密码、Remember-me Token,或者digest hash值等,按不同鉴权方式使用不同的 Authentication 实现,看源码可知道
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
注意 GrantedAuthority 该接口表示了当前用户所拥有的权限(或者角色)信息。这些信息由授权负责对象 AccessDecisionManager 来使用,并决定最终用户是否可以访问某 资源(URL或方法调用或域对象)。鉴权时并不会使用到该对象
UserDetails
这个接口规范了用户详细信息所拥有的字段,譬如用户名、密码、账号是否过期、是否锁定等。在Spring Security中,获取当前登录的用户的信息,一般情况是需要在这个接口上面进行 扩展,用来对接自己系统的用户,看源码可知
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetailsService
这个接口只提供一个接口 loadUserByUsername(String username) ,这个接口非常 重要, 一般情况我们都是通过 扩展 这个接口来显示获取我们的用户信息,用户登陆时传递的用户名和密码也是通过这里这查找出来的用户名和密码进行校验,但是真正的校验不在这里,而是由 AuthenticationManager 以及 AuthenticationProvider 负责的,需要强调的是,如果用户不存在,不应返回 NULL,而要抛出异常 UsernameNotFoundException,看源码可知
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
1.3 SpringSecurity快速入门
创建测试web工程
导入依赖
<dependencies>
<!--springMVC-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!--servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!--jsp-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
<!--jstl-->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!--jsckson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.9</version>
</dependency>
<!--文件上传-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!--spring相关坐标-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!--mybatis相关坐标-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.15</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<!--spring-mybatis坐标-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<!--分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version>
</dependency>
<!--junit单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--spring-security-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
</dependency>
<!--log4j依赖-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-security.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<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>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
</web-app>
spring-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
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
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<security:http auto-config="true" use-expressions="false" >
<!--
intercept-url定义一个过滤规则pattern表示对哪些url进行权限控制,ccess属性表示在请求对应的URL时需要什么权限,
默认配置时它应该是一个以逗号分隔的角色列表,请求的用户只需拥有其中的一个角色就能成功访问对应的URL-->
<security:intercept-url pattern="/**" access="ROLE_USER" />
<!--<security:form-login />-->
</security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user"
authorities="ROLE_USER" />
<security:user name="admin" password="{noop}admin"
authorities="ROLE_ADMIN" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
测试
1.4 SpringSecurity 使用自定义页面
添加以下页面
修改SpringSecurity.xml配置文件
<!-- 配置不过滤的资源(静态资源及登录相关) -->
<security:http security="none" pattern="/login.html" />
<security:http security="none" pattern="/failer.html" />
<security:http auto-config="true" use-expressions="false" >
<!-- 配置资料连接,表示任意路径都需要ROLE_USER权限 -->
<security:intercept-url pattern="/**" access="ROLE_USER" />
<!-- 自定义登陆页面,login-page 自定义登陆页面 authentication-failure-url 用户权限校验失败之后才会跳转到这个页面,如果数据库中没有这个用户则不会跳转到这个页面。
default-target-url 登陆成功后跳转的页面。 注:登陆页面用户名固定 username,密码 password,action:login -->
<security:form-login login-page="/login.html"
login-processing-url="/login"
username-parameter="username"
password-parameter="password" authentication-failure-url="/failer.html"
default-target-url="/success.html" authentication-success-forward-url="/success.html"
/>
<!-- 关闭CSRF,默认是开启的 -->
<security:csrf disabled="true" />
</security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user"
authorities="ROLE_USER" />
<security:user name="admin" password="{noop}admin"
authorities="ROLE_ADMIN" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
二、 用户登录
2.1 表结构分析与创建
-- 用户表
CREATE TABLE users(
id VARCHAR(32) PRIMARY KEY,
email VARCHAR(50) UNIQUE NOT NULL,
username VARCHAR(50),
PASSWORD VARCHAR(100),
phoneNum VARCHAR(20),
STATUS INT
);
-- 角色表
CREATE TABLE role(
id VARCHAR(32) PRIMARY KEY,
roleName VARCHAR(50) ,
roleDesc VARCHAR(50)
);
-- 用户角色关联表
CREATE TABLE users_role(
userId VARCHAR(32),
roleId VARCHAR(32),
PRIMARY KEY(userId,roleId),
FOREIGN KEY (userId) REFERENCES users(id),
FOREIGN KEY (roleId) REFERENCES role(id)
);
-- 资源权限表
CREATE TABLE permission(
id VARCHAR(32) PRIMARY KEY,
permissionName VARCHAR(50) ,
url VARCHAR(50)
);
-- 角色权限关联表
CREATE TABLE role_permission(
permissionId VARCHAR(32),
roleId VARCHAR(32),
PRIMARY KEY(permissionId,roleId),
FOREIGN KEY (permissionId) REFERENCES permission(id),
FOREIGN KEY (roleId) REFERENCES role(id)
);
2.2 创建实体类
创建Users
@Data
@NoArgsConstructor
public class Users{
private int id;
private String email;
private String username;
private String password;
private String phoneNum;
private int status;// '状态0 未开启 1 开启'
private String statusStr;
private Role role;
public String getStatusStr() {
if(status==0){
statusStr="未开启";
}else if(status==1){
statusStr="开启";
}
return statusStr;
}
}
创建Role
@Data
@NoArgsConstructor
public class Role {
private int id;
private String roleName;
private String roleDesc;
private List<Permission> permissions;
}
创建Permission
@Data
@NoArgsConstructor
public class Permission {
private int id;
private String permissionName;
private String url;
}
2.3 Spring Security使用数据库认证
在Spring Security中如果想要使用数据进行认证操作,有很多种操作方式,这里我们介绍使用UserDetails、
UserDetailsService来完成操作。
-
UserDetails
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
UserDetails是一个接口,我们可以认为UserDetails作用是于封装当前进行认证的用户信息,但由于其是一个
接口,所以我们可以对其进行实现,也可以使用Spring Security提供的一个UserDetails的实现类User来完成
操作
以下是User类的部分代码
public class User implements UserDetails, CredentialsContainer { private String password; private final String username; private final Set<GrantedAuthority> authorities; private final boolean accountNonExpired; //帐户是否过期 private final boolean accountNonLocked; //帐户是否锁定 private final boolean credentialsNonExpired; //认证是否过期 private final boolean enabled; //帐户是否可用 }
-
UserDetailsService
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
上面将UserDetails与UserDetailsService做了一个简单的介绍,那么我们具体如何完成Spring Security的数据库认证操作哪,我们通过用户管理中用户登录来完成Spring Security的认证操作。
2.4 用户登录流程分析
2.5 代码编写
编写login和failer页面
导入spring security相关坐标
<!--spring-security-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
配置web.xml
<!--加载spring环境 和 spring-security配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext*.xml,classpath:spring/spring-security.xml</param-value>
</context-param>
<!-- 额外添加springSecurity过滤器 -->
<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>
配置spring-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
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
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 配置不拦截的页面与静态资源 -->
<security:http pattern="/login.jsp" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/layui/**" security="none"/>
<!--
配置具体的规则
auto-config="true" 不用自己编写登录的页面,框架提供默认登录页面
use-expressions="false" 是否使用SPEL表达式
-->
<security:http auto-config="true" use-expressions="false">
<!-- 同源策略 如果页面使用iframe需要配置 否则不能使用 -->
<security:headers>
<security:frame-options disabled="true"/>
</security:headers>
<!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="访问系统的人,必须有ROLE_USER的角色" -->
<security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN" />
<!-- 定义跳转的具体的页面 -->
<security:form-login
login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"
authentication-success-forward-url="/index.jsp"
/>
<!-- 关闭跨域请求 -->
<security:csrf disabled="true"/>
<!-- 退出 -->
<security:logout invalidate-session="true" logout-url="/logout" logout-success-url="/login.jsp" />
</security:http>
<!-- 切换成数据库中的用户名和密码 -->
<security:authentication-manager>
<!-- user-service-ref="userService" springSecurity用于获取账号信息的类 需要实现UserDetailsService 重写加载数据方法 -->
<security:authentication-provider user-service-ref="userService">
<!-- 配置加密的方式 注意如果使用需要开启下面配置的加密类
<security:password-encoder ref="passwordEncoder"/>-->
</security:authentication-provider>
</security:authentication-manager>
<!-- 配置加密类 -->
<!--<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>-->
<!-- 提供了入门的方式,在内存中存入用户名和密码
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="admin" password="{noop}admin" authorities="ROLE_USER"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
-->
</beans>
spring security接收到用户名之后,spring security怎么就知道我们要调用的是那个service来完成用户的查询操作呢?
在配置文件中有这么一段配置:
<security:authentication-manager>
<security:authentication-provider user-service-ref="userService">
<!-- 配置加密的方式
<security:password-encoder ref="passwordEncoder"/>-->
</security:authentication-provider>
</security:authentication-manager>
user-service-ref
属性就是来指定要执行的service,当然这个service来继承 UserDetailsService,扩展我们自己的service接口,完成用户的认证的操作
编写UserMapper
public interface UserMapper {
//使用权限框架操作 根据账号查询信息
@Select("select * from users where username=#{username} and status=1")
public Users selectByUserName(String username);
}
编写UserService
public interface UserService extends UserDetailsService {
}
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据返回实体类对应数据
Users users = userMapper.selectByUserName(username);
if(users!=null){
//创建springSecurity主体中存储的账号对象并返回(账号,密码,权限列表)
//权限列表这里填null注意
UserDetails userDetails=new User(users.getUsername(),users.getPassword(),null);
return userDetails;
}
return null;
}
}
测试
在登录页面输入用户名 密码 跳转到了错误页面:
三、 用户登录问题分析
问题一:登录失败的问题
在数据库中是有jack这个用户的
为什么还是没办法登录,原因是在于我们的spring-security.xml配置文件中配置的问题:
在配置文件中,我们配置了
<!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="访问系统的人,必须有ROLE_USER的角色" -->
<security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN"/>
spring security 拦截了所有的请求,要想登录认证必须是有ROLE_USER,ROLE_ADMIN
两个权限的,在UserServiceImpl类中我将查询到的UserInfo
中的信息封装到了User
对象中,有一个值是为null
User user = new User(userInfo.getUsername(),userInfo.getPassword(), null);
也就是说我们查询出的这个对象是没有权限的,既是用户名和密码对了,没有权限也是没有办法登录的!
在这里我们需要模拟给出ROLE_USER,ROLE_ADMIN
两个权限,修改UserServiceImpl类
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据返回实体类对应数据
Users users = userMapper.selectByUserName(username);
if(users!=null){
//创建springSecurity主体中存储的账号对象并返回(账号,密码,权限列表)
UserDetails userDetails=new User(users.getUsername(),users.getPassword(),getAuthority());
return userDetails;
}
return null;
}
public List<SimpleGrantedAuthority> getAuthority(){
ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return simpleGrantedAuthorities;
}
}
修改完毕之后我们再次测试,看是否能登录成功!
这个报错的原因是spring security 默认使用的是密文提交的,现在没有进行加密! 我们只需要在获取密码的前面加上"{noop}" 代表的是使用明文
此时就认证成功
问题二:数据库查询用户角色
在上面的代码中我们的用户角色是自己手动添加的,这不是通用的一种方式,用户的角色我们要从数据库中进行查询!
修改UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.yanqi.ssm.mapper.UserMapper">
<!--通过用户名查询用户-->
<resultMap id="userResultMap" type="UserInfo" autoMapping="true">
<id property="id" column="id" />
<collection property="roles" ofType="Role" javaType="List" autoMapping="true">
<id property="id" column="rid" />
</collection>
</resultMap>
<select id="findUserByUserName" resultMap="userResultMap">
SELECT
* ,
r.id rid
FROM
users u,
role r,
users_role ur
WHERE
u.id = ur.userId AND
r.id = ur.roleId AND
u.username = #{username}
</select>
</mapper>
修改UserService
在这里需要注意的是,获取到对应的角色之后,其实还是不能登录,在users表中,有一个字段status
,这个字段表示的用户是否可能,0表示可用 1表示不可用。此时的功能原来使用的构造已经无法满足了,我们需要使用另外一个构造
public User(String username, String password,
/**
boolean enabled: 账户是否可用
boolean accountNonExpired:账户是否过期
boolean credentialsNonExpired:密码是否过期
boolean accountNonLocked:账户是否冻结被锁定
authorities:该账户所具有的权限
*/
boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
}
完整代码实现
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
UserInfo userInfo = userMapper.findUserByUserName(s);
User user = new User(userInfo.getUsername(),
"{noop}"+userInfo.getPassword(),
userInfo.getStatus() == 0 ? false : true ,
true,true,true,
getAuthority(userInfo.getRoles()));
return user;
}
public List<SimpleGrantedAuthority> getAuthority(List<Role> roles){
ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
for (Role role : roles) {
simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_"+role.getRoleName()));
}
return simpleGrantedAuthorities;
}
}
四、动态权限
在实际开发中一般通过对数据库表的操作实现动态的权限操作,SpringSecurity实现动态权限比较复杂需要自己实现过滤器与决策器,所以这里的内容是基于权限管理crud功能实现后的配置,没有进行详细的讲解,如需详细了解可以查看Spring Security认证与授权的原理
4.1 将公开权限设置为无需认证即可访问
<!-- 配置不拦截的资源 -->
<security:http pattern="/login.jsp" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/layui/**" security="none"/>
4.2 配置具体的规则
<!--
配置具体的规则
auto-config="true" 不用自己编写登录的页面,框架提供默认登录页面
use-expressions="true" 是否使用SPEL表达式(否则只能使用USER_角色的形式配置)
-->
<security:http auto-config="true" use-expressions="true">
<!-- 同源策略 如果页面使用iframe需要配置 否则不能使用 -->
<security:headers>
<security:frame-options disabled="true"/>
</security:headers>
<!-- 定义跳转的具体的页面 -->
<security:form-login
login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"
authentication-success-forward-url="/users/name"
/>
<security:intercept-url pattern="/**" access="isAuthenticated()"/>
<!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="isAuthenticated()"
<!-- 权限框架本质是过滤器链 会依次进行权限验证 如果验证通过继续执行
该配置 配置的是 除以上不拦截的资源外 所有url请求必须拥有认证的权限(登录后才能访问)
-->
<!-- 关闭跨域请求 -->
<security:csrf disabled="true"/>
<!-- 退出 -->
<security:logout invalidate-session="true" logout-url="/logout" logout-success-url="/login.jsp" />
</security:http>
2.3 设置权限数据
4.4 自定义相关过滤器与决策器
自定义决策管理器
权限框架本身是由多个不同功能的过滤器组成的,不同的过滤器负责不同的功能例如认证过滤、静态资源过滤、等 授权过滤也是一样,决策器就是同于判断当前请求的url当前账号是否拥有权限
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Iterator;
@Service
//决策管理器
public class MyAccessDecisionManager implements AccessDecisionManager {
// decide 方法是判定是否拥有权限的决策方法,
//authentication 是释CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
//object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
//configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== configAttributes || configAttributes.size() <=0) {
return;
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
if(needRole.trim().equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("no right");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
自定义权限加载器
import com.yunhe.javabean.Permission;
import com.yunhe.mapper.PermissionMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@Service
//授权管理器
//用于当前项目要动态配置的权限信息
//从数据库中取出所有的权限信息 进行配置
//这样当客户请求对应权限时进行权限验证(因为不可能将所有的url都过滤)
public class MyInvocationSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired
//注入权限查询的dao层
private PermissionMapper permissionMapper;
private HashMap<String, Collection<ConfigAttribute>> map =null;
/**
* 加载权限表中所有权限
*/
public void loadResourceDefine(){
map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
//动态查询当前数据库中所有的权限
List<Permission> permissions = permissionMapper.selectAll();
for(Permission permission : permissions) {
array = new ArrayList<>();
cfg = new SecurityConfig(permission.getPermissionName());
//此处只添加了权限的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
array.add(cfg);
//用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
//实际加载存储的结构为 url->[name1,name2....]
//url是进行请求url拦截使用的 name是进行权限验证使用的
//也就是说在用户进行授权时 实际加载的是权限名称
map.put(permission.getUrl(), array);
}
}
//此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(map ==null) loadResourceDefine();
//object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
AntPathRequestMatcher matcher;
String resUrl;
for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
resUrl = iter.next();
matcher = new AntPathRequestMatcher(resUrl);
if(matcher.matches(request)) {
return map.get(resUrl);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
自定义权限拦截器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;
import javax.servlet.*;
import java.io.IOException;
@Service
//自定义权限拦截器
//FilterSecurityInterceptor是权限框架中用于处理权限验证的过滤器
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
//使用自己定义权限加载器
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
//使用自定义的决策管理器
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
4.5 将自定义权限拦截器配置入权限框架中
在security:http标签中添加
<!-- 配置自定义拦截器 将定义拦截器配置到当前权限框架拦截器链中的指定位置
将其配置在本身的权限认证过滤器之后执行
-->
<security:custom-filter ref="myFilterSecurityInterceptor" after="FILTER_SECURITY_INTERCEPTOR"></security:custom-filter>
4.6 配置授权页面
在配置后使用账号登录会出现没有权限403代码页面
如果用户登录进行操作时,直接报403错误用户体检并不好,我们可以制作一个比较好看的页面,告诉用户您用户不足,请联系管理员!
在web.xml中配置
<error-page>
<error-code>403</error-code>
<location>/failer.jsp</location>
</error-page>
4.7 授权
在书写权限加载器时,我们发现,加载器加载url是用于http请求的过滤匹配,而实际进行权限验证使用的是权限名称,为了方便书写,我们在登录认证时查询权限信息进行授权操作
修改PermissionMapper
//根据用户id查询权限数据
public List<Permission> selectByUid(int uid);
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yunhe.mapper.PermissionMapper">
<select id="selectByUid" resultType="com.yunhe.javabean.Permission">
select p.*
from users u,role r,permission p,users_role ur,role_permission rp
where u.id=ur.userId and r.id=ur.roleId and r.id=rp.roleId and p.id =rp.permissionId and u.id=#{uid}
</select>
</mapper>
修改PermissionService
//根据uid查询权限数据
public List<Permission> findByUid(int uid);
@Override
public List<Permission> findByUid(int uid) {
return permissionMapper.selectByUid(uid);
}
修改userService
将我们之前书写写死的角色认证与权限认证查询数据库的形式添加
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据返回实体类对应数据
Users users = userMapper.selectByUserName(username);
if (users != null) {
//创建springSecurity主体中存储的账号对象并返回(账号,密码,权限列表)
UserDetails userDetails = new User(users.getUsername(), users.getPassword(), getAuthority(users.getId()));
return userDetails;
}
return null;
}
@Autowired
PermissionService permissionService;
//获取权限列表
public List<GrantedAuthority> getAuthority(int uid) {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//查询指定用户权限列表
List<Permission> permissionList = permissionService.findByUid(uid);
for (Permission p:permissionList ) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(p.getPermissionName());
grantedAuthorities.add(grantedAuthority);
}
return grantedAuthorities;
}