1. 前言
1.1 SpringSecurity 框架用法简介
用户登录系统时我们协助 SpringSecurity 把用户对应的角色、权限组装好,同时把各个 资源所要求的权限信息设定好,剩下的“登录验证”、“权限验证”等等工作都交给 SpringSecurity
1.2 权限管理过程中的相关概念
1.2.1 主体
英文单词:principal
使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统 谁就是主体
1.2.2 认证
英文单词:authentication
权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明 自己是谁。 笼统的认为就是以前所做的登录操作。
1.2.3 授权
英文单词:authorization
将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的 能力。所以简单来说,授权就是给用户分配权限。
1.3 security的特点
Spring 技术栈的组成部分,通过提供完整可扩展的认证和授权支持保护你的应用程序。官网地址:
https://spring.io/projects/spring-security
SpringSecurity 特点:
1)和 Spring 无缝整合。
2) 全面的权限控制。
3)专门为 Web 开发而设计。
4)旧版本不能脱离 Web 环境使用。
5)新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独 引入核心模块就可以脱离 Web 环境。
6)重量级
2. 环境准备
2.1 新建工程
2.1.1 引入SpringMVC依赖
<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.bjc</groupId>
<artifactId>springsecuryti-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.20.RELEASE</version>
</dependency>
<!-- 引入 Servlet 容器中相关依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!-- JSP 页面使用的依赖 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1.3-b06</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
2.1.2 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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<context:component-scan
base-package="com.bjc.security"></context:component-scan>
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:default-servlet-handler />
</beans>
2.1.3 web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<servlet>
<servlet-name>springDispatcherServlet</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>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Map all requests to the DispatcherServlet for handling -->
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
然后引入静态资源等文件。访问,如图:
2.2 引入security框架
2.2.1 引入security依赖
<!-- 添加security依赖 -->
<!-- SpringSecurity 对 Web 应用进行权限管理 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
<!-- SpringSecurity 配置 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
<!-- SpringSecurity 标签库 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.2.10.RELEASE</version>
</dependency>
2.2.2 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>
2.2.3 security配置类
package com.bjc.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{
}
然后启动项目,访问,如图:
发现,需要输入用户名和密码才能进入首页了,这说明security已经生效了。
3. 放行首页和静态资源
我们只需要在配置类中重写父类的configure方法即可,例如:
@Configuration
@EnableWebSecurity // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 对请求进行授权
.antMatchers("/index.jsp") // 针对 /index.jsp路径进行授权
.permitAll() // 可以无条件访问
.antMatchers("/layui/**") // 针对 /layui目录下的静态资源进行授权
.permitAll() // 设置其可以无条件访问
.and()
.authorizeRequests() // 对请求进行授权
.anyRequest() // 任意的请求
.authenticated() // 都需要登录之后才能访问
;
}
}
再次访问系统,如图:
可以正常访问了。
4. 指定登录页面
4.1 无权限跳转到自带的登录页
使用formLogin
访问无权限的野蛮,如图:
4.2 无权限跳转到指定的登录页
4.2.1 loginPage
通过loginPage方法指定登录页面(如果没有指定会访问security自带的登录页面),其可以修改security默认的登录请求地址
指定登录页前后 SpringSecurity 登录地址变化:
指定前 |
/login GET - the login form
|
/login POST - process the credentials and if valid authenticate the user
| |
/login?error GET - redirect here for failed authentication attempts
| |
/login?logout GET - redirect here after successfully logging out
| |
指定后 |
/index.jsp GET - the login form
|
/index.jsp POST - process the credentials and if valid authenticate the user
| |
/index.jsp?error GET - redirect here for failed authentication attempts
| |
/index.jsp?logout GET - redirect here after successfully logging out
|
例如:
4.2.2 loginProcessingUrl
通过调用 loginProcessingUrl()方法指定登录地址。如果指定了loginProcessingUrl,那么其值就会覆盖loginPage方法中设置的默认值(index.jsp POST)
例如:
4.3 设置登录用户名和密码
实现思路,如图:
通过上面的分析,我们知道,我们要实现登录,我们需要form表单的用户名和密码的name属性是一个约定好的值。如果是采用默认的,就是
SpringSecurity 默认账号的请求参数名:username
SpringSecurity 默认密码的请求参数名:password
如果需要定制的话,我们需要配置一下。
4.3.1 定制登录用户名密码
1) 我们可以在配置类中指定参数,并指定登录成功跳转的路径例如:
注意:loginProcessingUrl后面的permitAll可以省略不要。
2)在jsp中,指定登录用户名和密码的参数名,如图:
注意:标签
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
需要加上,用于跨域验证的。在配置版中有过csrf的介绍
3)配置授权信息
在配置类中,重写另一个configure方法,并设置用户名和密码以及对应的角色和权限,例如:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication() // 内存授权
.withUser("tom") // 设置账号
.password("123456") // 设置密码
.roles("ADMIN") // 设置角色
.and()
.withUser("marry") // 设置另一个用户
.password("123456") // 密码
.authorities("SAVE","EDIT") // 设置权限
;
}
使用账号tom/123456访问,可以成功的进入系统,如图:
目前为止完整的配置类如下:
package com.bjc.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{
// 授权
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication() // 内存授权
.withUser("tom") // 设置账号
.password("123456") // 设置密码
.roles("ADMIN") // 设置角色
.and()
.withUser("marry") // 设置另一个用户
.password("123456") // 密码
.authorities("SAVE","EDIT") // 设置权限
;
}
// 认证
@Override
protected void configure(HttpSecurity security) throws Exception {
security
.authorizeRequests() // 对请求进行授权
.antMatchers("/index.jsp") // 针对 /index.jsp路径进行授权
.permitAll() // 可以无条件访问
.antMatchers("/layui/**") // 针对 /layui目录下的静态资源进行授权
.permitAll() // 设置其可以无条件访问
.and()
.authorizeRequests() // 对请求进行授权
.anyRequest() // 任意的请求
.authenticated() // 都需要登录之后才能访问
.and()
.formLogin() // 无权限跳转到自带的登录页
/**
* 关于loginPage方法的特殊说明:
* 指定登录页的同时会影响到:提交表单的地址、退出登录地址、登录失败的地址
* 如果指定loginPage的值为/index.jsp,那么,
* 1)去登录页:/index.jsp Get
* 2)提交登录表单:/index.jsp POST
* 3)登录失败:/index.jsp?error
* 4)退出登录:/index.jsp?logout
* */
.loginPage("/index.jsp") // 指定登录页面(如果没有指定会访问security自带的登录页面)
/*
* loginProcessingUrl的说明:
* 如果指定了loginProcessingUrl,那么其值就会覆盖loginPage方法中设置的默认值(index.jsp POST)
* */
.loginProcessingUrl("/do/login.html") // 指定提交登录表单的地址
.permitAll() // 允许登录地址访问(可以不要)
.usernameParameter("loginAcct") // 指定登录账号的请求参数名
.passwordParameter("loginPwd") // 指定登录密码号的请求参数名
.defaultSuccessUrl("/main.html") // 设置登录成功去到的页面
;
}
}
4.4 退出登录
4.4.1 禁用csrf的退出登录
csrf是security默认开启的,我们需要禁用掉它。如果 CSRF 功能没有禁用,那么退出请求必须是 POST 方式。如果禁用了 CSRF 功能则任何请求方式都可以。所以,我们可以直接使用a标签并指定退出url即可实现退出功能了。
我们可以使用logout()方法开启注销功能,使用logoutUrl()方法来自定义注销功能的 URL 地址。
1)配置
2)页面
<a id="logoutAnchor" href="${pageContext.request.contextPath }/do/logout.html">退出</a>
4.4.2 启用csrf功能退出登录
如果 CSRF 功能没有禁用,那么退出请求必须是 POST 方式。
1)配置
2)form表单
<li class="layui-nav-item">
<form id="logoutForm" action="${pageContext.request.contextPath }/do/logout.html" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
<a id="logoutAnchor" href="">退出</a>
<script type="text/javascript">
window.onload = function() {
// 点击a标签,提交表单
document.getElementById("logoutAnchor").onclick = function() {
document.getElementById("logoutForm").submit();
// 阻止a标签的默认执行动作
return false;
};
};
</script>
</li>
同时,我们可以自定义退出登录的处理器
1)addLogoutHandler()方法:添加退出处理器
2)logoutSuccessHandler()方法:退出成功处理器
5. 基于角色或权限访问控制
我们可以给特定资源指定特定的权限或者角色才能访问。例如:
5.1 指定特定的资源需要哪些角色或者权限
注意:要先设置小范围的授权在设置大范围的,即anyRequest()放在后面,如上图。
5.2 指定用户具备的权限或者角色
所以,用户ton可以访问level1目录的内容,marry可以访问level2目录下的内容
如图:
1)tom用户
访问level1下的内容
访问level2下的内容
2)marry用户
访问lever1目录下的内容
访问level2目录下的内容
注意:SpringSecurity 会在角色字符串前面加“ROLE_”前缀,查看security源码,如图:
之所以要强调这个事情,是因为将来从数据库查询得到的用户信息、角色信息、权 限信息需要我们自己手动组装。手动组装时需要我们自己给角色字符串前面加“ROLE_” 前缀。
5.3 自定义403页面
当我们访问的资源没有权限的时候,security默认给我们跳转到一个403的错误页面,这个体验极度不好,所以,我们需要自定义该页面。
5.3.1 访问被拒绝之后直接跳转到页面
controller
@RequestMapping("/to/no/auth/page.html")
public String toNoAuthPage() {
return "no_auth";
}
效果:
5.3.2 访问被拒绝后走处理器
该方式可以带一些数据到页面,当然,上一种也可以,只是带的数据是通用的数据,该方式可以根据不同的异常携带不同的数据,我们可以通过accessDeniedHandler方法来定制。
例如:
访问没权限的资源,如:
6. 记住我功能(不重要)
6.1 内存版本
HttpSecurity 对象调用 rememberMe()方法,登录表单携带名为 remember-me 的请求参数即可。例如:
页面
这时候,在登录页,会有一个记住我的选择框,勾选之后,发起请求,就会在浏览器写入一个名为remember-me的cookie,再次在登录的时候,就根据这个 Cookie 的 value 在服务器端找到以前登录的 User,而且这个 Cookie 被设置为存储 2 个星期。
当我们关闭浏览器,再次访问的时候,就可以直接进入系统了,不需要在输入用户名和密码。
6.2 数据库版(不重要)
为了让服务器重启也不影响记住登录状态,将用户登录状态信息存入数据库
6.2.1 引入数据库相关依赖
<!-- 引入数据库相关依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<!-- mysql 驱动 --> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.3.20.RELEASE</version>
</dependency>
6.2.2 配置数据源
<!-- 配置数据源 -->
<bean id="dataSource"
class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="url" value="jdbc:mysql://localhost:3306/security?useSSL=false"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
<!-- jdbcTemplate -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
6.2.3 创建数据库
CREATE DATABASE `security` CHARACTER SET utf8;
CREATE TABLE persistent_logins (
username VARCHAR (64) NOT NULL,
series VARCHAR (64) PRIMARY KEY,
token VARCHAR (64) NOT NULL,
last_used TIMESTAMP NOT NULL
) ;
6.2.4 配置类
1)在 WebAppSecurityConfig 类中注入数据源
@Autowired
private DataSource dataSource;
2)启用令牌仓库功能
repository是JdbcTokenRepositoryImpl对象
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
然后,重启服务器,登录成功,查看数据库,如图:
数据已经正确存入数据库,再次重启服务器,访问,一样可以不用登录就可以进入系统。
注意:表来自于JdbcTokenRepositoryImpl类,如图:
既然该类已经有了建表语句,为什么我们还需要手动建表了,我们看源码,如图:
有一个init方法,可以自动创建表,但是,问题来了,该方法用protected修饰的,该类是框架提供的,我们只能继承该类,才能调用到该方法,但是,这样做比较不划算,所以,我们还不如自己创建一个表算了。
完整的配置类:
package com.bjc.security.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
@Configuration
@EnableWebSecurity // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private DataSource dataSource;
// 授权 确定用户有什么权限和角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication() // 内存授权
.withUser("tom") // 设置账号
.password("123456") // 设置密码
.roles("ADMIN","门徒") // 设置角色
.and()
.withUser("marry") // 设置另一个用户
.password("123456") // 密码
.authorities("SAVE","UPDATE") // 设置权限
;
}
// 认证 确定资源需要什么权限和角色才能访问
@Override
protected void configure(HttpSecurity security) throws Exception {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
security
.authorizeRequests() // 对请求进行授权
.antMatchers("/index.jsp") // 针对 /index.jsp路径进行授权
.permitAll() // 可以无条件访问
.antMatchers("/layui/**") // 针对 /layui目录下的静态资源进行授权
.permitAll() // 设置其可以无条件访问
// .and()
// .authorizeRequests() // 对请求进行授权
.antMatchers("/level1/**") // 访问level1下的资源
.hasRole("门徒") // 需要角色是门徒的才能访问
.antMatchers("/level2/**") // 访问level2下的资源
.hasAuthority("UPDATE") // 需要用户具备UPDATE权限才能访问
.anyRequest() // 其他未设置的全部请求
.authenticated() // 都需要登录之后才能访问
.and()
.formLogin() // 无权限跳转到自带的登录页
/**
* 关于loginPage方法的特殊说明:
* 指定登录页的同时会影响到:提交表单的地址、退出登录地址、登录失败的地址
* 如果指定loginPage的值为/index.jsp,那么,
* 1)去登录页:/index.jsp Get
* 2)提交登录表单:/index.jsp POST
* 3)登录失败:/index.jsp?error
* 4)退出登录:/index.jsp?logout
* */
.loginPage("/index.jsp") // 指定登录页面(如果没有指定会访问security自带的登录页面)
/*
* loginProcessingUrl的说明:
* 如果指定了loginProcessingUrl,那么其值就会覆盖loginPage方法中设置的默认值(index.jsp POST)
* */
.loginProcessingUrl("/do/login.html") // 指定提交登录表单的地址
.permitAll() // 允许登录地址访问(可以不要)
.usernameParameter("loginAcct") // 指定登录账号的请求参数名
.passwordParameter("loginPwd") // 指定登录密码号的请求参数名
.defaultSuccessUrl("/main.html") // 设置登录成功去到的页面
.and()
//.csrf()
//.disable() // 禁用csrf功能
.logout() // 开启退出功能
.logoutUrl("/do/logout.html") // 指定退出url
.logoutSuccessUrl("/index.jsp") // 指定退出成功之后去到的url
.and()
.exceptionHandling() // 指定异常处理器
// .accessDeniedPage("/to/no/auth/page.html") // 访问被拒绝的时候,前往的页面
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 携带数据到页面,页面用${message}接收数据
request.setAttribute("message", "抱歉,您没有权限访问该页面,请找管理员配置合适的权限在访问。。。");
// 转发
request.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(request, response);
}
})
.and()
.rememberMe() // 记住我
.tokenRepository(repository);
;
}
}
7. 数据库登录
前面我们的登录是采用的内存登录方式,适用于系统指定用户且用户量较少的系统适用,现在我们需要查询数据库,适用数据库中的用户来进行登录。
7.1 默认实现(了解)
SpringSecurity 的默认实现已经将 SQL 语句硬编码在了 JdbcDaoImpl 类中,所以,默认方法显得比较刻板,源码如下:
可以知道,默认实现表结构等都是规定好的,相当不方便。这种 情况下,我们有下面三种选择
1)按照 JdbcDaoImpl 类中 SQL 语句设计表结构。
2)修改 JdbcDaoImpl 类的源码。
3)不使用 jdbcAuthentication()。
auth.jdbcAuthentication().usersByUsernameQuery("tom")
7.2 自定义数据库查询方式
自定义数据库查询方式也很简单,只需要执行auth.userDetailsService(userDetailsService);即可,其中userDetailsService需要自定义实现UserDetailsService接口的类并自动装配。
7.2.1 自定义userDetailsService
package com.bjc.security.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import com.bjc.security.entity.Admin;
@Component
public class MyUserDetailServiceImpl implements UserDetailsService{
@Autowired
private JdbcTemplate jdbcTemplate;
// 总目标:根据表单提交的用户名查询User对象,并装配角色、权限等信息
// username 表单提交的用户名
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.从数据库查询Admin对象
String sql = "SELECT id,loginacct,userpswd,username,email FROM t_admin WHERE loginacct=?";
List<Admin> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Admin.class), username);
Admin admin = list.get(0);
// 2.给Admin设置角色权限信息
List<GrantedAuthority> authorities = new ArrayList<>();
// 角色前面需要添加ROLE_ 这里应该查询对应的角色表的
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
// 权限前面不需要添加前缀
authorities.add(new SimpleGrantedAuthority("UPDATE"));
// 3.把admin对象和authorities封装到UserDetails中
String userpswd = admin.getUserpswd();
// User是UserDetails的实现类
return new User(username, userpswd, authorities);
}
}
7.2.2 设置
访问,使用数据库中的用户名和密码,可以成功访问。
8. 密码加密
加密我们需要通过passwordEncoder方法指定加密算法。
8.1 普通MD5加密
8.1.1 加密算法类
package com.bjc.security.config;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
*加密类
*
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
/**
* 加密方法:采用MD5加密
*/
@Override
public String encode(CharSequence rawPassword) {
return generatePwd(rawPassword);
}
/**
* 校验方法
* 1. rawPassword :明文
* 2. encodedPassword:数据库查询出来的密文
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(generatePwd(rawPassword));
}
// 加密方法
private String generatePwd(CharSequence rawPassword) {
String pwd = (String)rawPassword;
try {
// 获取MD5加密类
MessageDigest instance = MessageDigest.getInstance("MD5");
// 得到加密后的密码字节数组
byte[] afterPwd = instance.digest(pwd.getBytes());
// 转换成16位
return new BigInteger(1,afterPwd).toString(16);
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
8.1.2 配置
1)引入加密算法
2)设置加密算法
8.2 带盐值的加密
SpringSecutiry中,给我们提供了一个类bCryptPasswordEncoder,我们可以很方便的对密码进行加密,其比Md5更安全,采用的是动态“盐”机制。
使用起来很简单,直接将我们自定义的那个加密类替换成bCryptPasswordEncoder即可。
例如:
注意:我们需要将该类注入到spring容器中
也可以直接调用getBCryptPasswordEncoder()传参。
配置类最终版
package com.bjc.security.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
@Configuration
@EnableWebSecurity // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private DataSource dataSource;
@Autowired
private MyUserDetailServiceImpl userDetailsService;
/*
* @Autowired private MyPasswordEncoder passwordEncoder;
*/
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// 授权 确定用户有什么权限和角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*auth
.inMemoryAuthentication() // 内存授权
.withUser("tom") // 设置账号
.password("123456") // 设置密码
.roles("ADMIN","门徒") // 设置角色
.and()
.withUser("marry") // 设置另一个用户
.password("123456") // 密码
.authorities("SAVE","UPDATE") // 设置权限
;*/
// 装配userDetailsService
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
// 认证 确定资源需要什么权限和角色才能访问
@Override
protected void configure(HttpSecurity security) throws Exception {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
security
.authorizeRequests() // 对请求进行授权
.antMatchers("/index.jsp") // 针对 /index.jsp路径进行授权
.permitAll() // 可以无条件访问
.antMatchers("/layui/**") // 针对 /layui目录下的静态资源进行授权
.permitAll() // 设置其可以无条件访问
// .and()
// .authorizeRequests() // 对请求进行授权
.antMatchers("/level1/**") // 访问level1下的资源
.hasRole("门徒") // 需要角色是门徒的才能访问
.antMatchers("/level2/**") // 访问level2下的资源
.hasAuthority("UPDATE") // 需要用户具备UPDATE权限才能访问
.anyRequest() // 其他未设置的全部请求
.authenticated() // 都需要登录之后才能访问
.and()
.formLogin() // 无权限跳转到自带的登录页
/**
* 关于loginPage方法的特殊说明:
* 指定登录页的同时会影响到:提交表单的地址、退出登录地址、登录失败的地址
* 如果指定loginPage的值为/index.jsp,那么,
* 1)去登录页:/index.jsp Get
* 2)提交登录表单:/index.jsp POST
* 3)登录失败:/index.jsp?error
* 4)退出登录:/index.jsp?logout
* */
.loginPage("/index.jsp") // 指定登录页面(如果没有指定会访问security自带的登录页面)
/*
* loginProcessingUrl的说明:
* 如果指定了loginProcessingUrl,那么其值就会覆盖loginPage方法中设置的默认值(index.jsp POST)
* */
.loginProcessingUrl("/do/login.html") // 指定提交登录表单的地址
.permitAll() // 允许登录地址访问(可以不要)
.usernameParameter("loginAcct") // 指定登录账号的请求参数名
.passwordParameter("loginPwd") // 指定登录密码号的请求参数名
.defaultSuccessUrl("/main.html") // 设置登录成功去到的页面
.and()
//.csrf()
//.disable() // 禁用csrf功能
.logout() // 开启退出功能
.logoutUrl("/do/logout.html") // 指定退出url
.logoutSuccessUrl("/index.jsp") // 指定退出成功之后去到的url
.and()
.exceptionHandling() // 指定异常处理器
// .accessDeniedPage("/to/no/auth/page.html") // 访问被拒绝的时候,前往的页面
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 携带数据到页面,页面用${message}接收数据
request.setAttribute("message", "抱歉,您没有权限访问该页面,请找管理员配置合适的权限在访问。。。");
// 转发
request.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(request, response);
}
})
.and()
.rememberMe() // 记住我
.tokenRepository(repository);
;
}
}
9. security标签
我们需要在页面中显示security提供的信息,可以使用security提供的标签库来显示相应的信息,例如,显示登录用户的名称
9.1 导入标签库
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
9.2 使用标签
如图,security给我们提供了如下几个标签
以security:authentication为例,标签写法如下:
<security:authentication property="xxx"/>
property属性为表达式,这个表达式怎么写了,我们暂时用xxx代替,看页面显示什么,启动项目,如图:
页面报错了,我们看最下面的错误信息,如图:
可以知道,该标签获取的属性是类UsernamePasswordAuthenticationToken中的属性,打开该类,如图,发现了2个get方法
我们在页面中获取一下principal(主体),看看它是何方神圣,如图:
SecurityAdmin是我们定义的一个继承了User的类,由此可以得出一个结论:
SpringSecurity处理完登录操作之后把登录成功的User对象以principal属性名存入了UsernamePasswordAuthenticationToken对象
知道了这里的主体是什么了,表达式就好写了,例如:
显示用户昵称:
<security:authentication property="principal.originalAdmin.userName"/>
注意:这里没有使用credentials,是因为credentials是密码擦除,最后程序将其置为了null,所以页面访问会报错。
10 权限控制
10.1 使用角色控制
10.1.1 配置方式
指定某个连接必须具有某个角色或者权限才能访问
10.1.2 注解方式
为了方便,我们往往在控制层(Controller)使用注解来进行权限控制,值得注意的是,使用注解方式,需要在配置类中开启注解权限功能
1) 开启注解权限功能
在配置类上加上如下注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
例如:
2) 在需要控制的地方加上注解
例如:我们希望访问/role/getPageInfo.json需要有部长的角色才能访问
我们只需要在方法上添加如下注解
@PreAuthorize("hasRole('部长')")
例如:
10.2 使用权限进行权限控制
同使用角色,只是单词不同,例如:
1)配置类方式
.antMatchers("/admin/getByPageInfo.html")
.hasAuthority("user:get")
2)注解方式
@PreAuthorize("hasAuthority('user:get')")
10.3 使用角色与权限进行权限控制
在配置中,我们可以access来使用角色与权限联合权限控制
例如:只有是经理角色,并具有user:get的用户才能访问
.access("hasRole('经理') OR hasAuthority('user:get')")
11. 其他注解
11.1 @PostAuthorize
先执行方法然后根据方法返回值判断是否具备权限
例如:查询一个 Admin 对象,在@PostAuthorize 注解中和当前登录的 Admin 对象 进行比较,如果不一致,则判断为不能访问。实现“只能查自己”效果。
@PostAuthorize("returnObject.data.loginAcct == principal.username")
注意:使用 returnObject 获取到方法返回值,使用 principal 获取到当前登录用户的主体对 象
11.2 @PreFilter
在方法执行前对传入的参数进行过滤。只能对集合类型的数据进行过滤。
@PreFilter(value="filterObject%2==0")
@ResponseBody
@RequestMapping("/admin/test/pre/filter")
public ResultEntity<List<Integer>> saveList(@RequestBody List<Integer> valueList) {
return ResultEntity.successWithData(valueList);
}
11.3 @PostFilter
在方法执行后对方法返回值进行过滤。只能对集合类型的数据进行过滤。
12. 页面元素的权限控制
我们可以使用标签根据用户不同的权限来显示不同的内容,当然,前提是我们需要引入标签库
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
使用标签<security:authorize>来实现,例如:
<!-- 有经理角色的才能看到 -->
<security:authorize access="hasRole('经理')">
<div class="col-xs-6 col-sm-3 placeholder">
<img data-src="holder.js/200x200/auto/sky" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</security:authorize>
<!-- 有部长角色的才能看到 -->
<security:authorize access="hasRole('部长')">
<div class="col-xs-6 col-sm-3 placeholder">
<img data-src="holder.js/200x200/auto/vine" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</security:authorize>
<!-- 有user:get权限的才能看到 -->
<security:authorize access="hasAuthority('user:get')">
<div class="col-xs-6 col-sm-3 placeholder">
<img data-src="holder.js/200x200/auto/sky" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</security:authorize>