Spring Security
Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。
在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。
自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。
因此,一般来说,常见的安全管理技术栈的组合是这样的(推荐):
- SSM + Shiro
- Spring Boot/Spring Cloud + Spring Security
SSM和SpringSecurity的配置
导包
<!--Spring security-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.3.2.RELEASE</version>
</dependency>
配置web.xml
<!--SpringSecurity核心过滤器链-->
<!--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>
配置spring-security.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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--配置springSecurity-->
<!--
auto-config="true" 表示自动加载 springSecurity 的配置文件
use-expressions="true" 表示使用spring的el表达式来配置springSecurity
-->
<security:http auto-config="true" use-expressions="true">
<!--让认证页面可以匿名访问-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--拦截资源
pattern="/**" 表示拦截所有资源
access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
</security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="Jsu" password="{noop}0000"
authorities="ROLE_USER"/>
<security:user name="Admin" password="{noop}0000"
authorities="ROLE_Admin"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
将spring-security.xml 配置文件引入applicationContext.xml中
<!-- 引入SpringSecurity配置文件-->
<import resource="classpath:spring-security.xml"/>
SpringSecurity使用自定义认证页面
spring-security.xml配置
<!--释放静态资源-->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<!--配置springSecurity-->
<!--
auto-config="true" 表示自动加载 springSecurity 的配置文件
use-expressions="true" 表示使用spring的el表达式来配置springSecurity
-->
<security:http auto-config="true" use-expressions="true">
<!--让认证页面可以匿名访问-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--拦截资源
pattern="/**" 表示拦截所有资源
access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
<!--配置认证信息
login-page 自定义登录页面
login-processing-url: 映射默认的/login
default-target-url:登录成功后 默认进入的页面
authentication-failure-url:登录失败 进入的页面
-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"/>
</security:http>
页面修改
<form action="${pageContext.request.contextPath}/login" method="post">
CSRF防护机制
CSRF(Cross-site request forgery) 跨站请求伪造,是一种难以防范的网络攻击方法
SpringSecurity中CsrfFilter过滤器
SpringSecurity的csrf机制将请求方式分为两类处理
- 第一类:“GET”,“HEAD”,“TRACE”,"OPTIONS"四类请求可以直接通过
- 第二类:除了上面四类,包括POST都要被验证携带token才可以通过
方法
- 方法一:直接禁用csrf,不推荐
- 方法二:在认证页面携带token请求
禁用csrf防护机制
<security:http auto-config="true" use-expressions="true">
<!--去掉csrf拦截的过滤器-->
<security:csrf disabled="true"/>
</security:http>
认证页面携带token
<%-- 在头部加上 --%>
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
<%-- form表单中加 --%>
<security:csrfInput/>
在这里插入图片描述
SpringSecurity使用数据库数据完成认证(加密)
配置spring-security.xml
<!--把加密对象放入的IOC容器中 passwordEncoder-->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!--设置Spring Security认证用户信息的来源-->
<!--
springSecurity默认的认证必须是加密的,加上{noop}表示不加密认证
-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
UserService
// 继承UserDetailsService
public interface UserService extends UserDetailsService {
}
UserServiceImpl
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleService roleService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void save(SysUser user) {
// 加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
userDao.save(user);
}
/**
* 功能描述: 认证业务
* @Param: [username] 用户在浏览器中输入的用户名
* @Return: UserDetails 是springSecurity自己的用户对象
* @Author: Jsu
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try { // 避免有两个username相同的
// 根据用户名进行查询
SysUser sysUser = userDao.findByName(username);
// 查询失败
if (sysUser == null) {
// 认证失败
return null;
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 将当前用户的角色添加进入
List<SysRole> roles = sysUser.getRoles();
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
//{noop}+sysUser.getPassword() 不被加盐加密 sysUser.getPassword() 加盐加密
UserDetails userDetails = new User(sysUser.getUsername(),sysUser.getPassword(),authorities);
return userDetails;
} catch (Exception e) {
e.printStackTrace();
// 认证失败
return null;
}
}
}
用户状态Status
在开发中有些用户 会被禁用 但是不会删除数据库
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try { // 避免有两个username相同的
// 根据用户名进行查询
SysUser sysUser = userDao.findByName(username);
// 查询失败
if (sysUser == null) {
// 认证失败
return null;
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 将当前用户的角色添加进入
List<SysRole> roles = sysUser.getRoles();
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
// {noop} 后面的密码 会认为是原文
// boolean ernabled是否可用
// boolean accountNonExpired账户是否失效
// boolean credentialsNonExpired秘密是否失效
// boolean accountNonLocked账户是否锁定
// 四个都为true 就运行
UserDetails userDetails = new User(sysUser.getUsername(),
sysUser.getPassword(),
sysUser.getStatus() == 1, // 重点!!!!
true,
true,
true,
authorities);
return userDetails;
} catch (Exception e) {
e.printStackTrace();
// 认证失败
return null;
}
}
RemberMe
重!!!name=“remember-me”value=“true”/“1”/“on”/"yes"都可以
<label><input type="checkbox" name="remember-me" value="true"> 记住 下次自动登录</label>
<!--Spring-security.xml-->
<security:http auto-config="true" use-expressions="true">
<!-- 开启remember-me 过滤器 设置token存储的时间-->
<security:remember-me token-validity-seconds="60"/>
</security:http>
如何持久化
存数据库中(官方规定写法 固定不变!!)
CREATE TABLE `persistent_logins`(
username varchar(64) NOT NULL,
series varchar(64) NOT NULL,
token varchar(64) NOT NULL,
last_used timestamp NOT NULL,
PRIMARY KEY (series)
)ENGINE=InnoDB DEFAULT CHARSET=utf8
<!--Spring-security.xml 配上数据库连接池-->
<security:remember-me
data-source-ref="dataSource"
remember-me-parameter="remember-me"
token-validity-seconds="60"/>
显示当前认证用户名
<span class="hidden-xs">
<security:authentication property="name" />
</span>
或者
<span class="hidden-xs">
<security:authentication property="principal.username" />
</span>
SpringBoot整合SpringSecurity
导包
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.1.3.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- springboot 引入tomcat jsp文件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.0.RELEASE</version>
<configuration>
<mainClass>com.jsu.SpringSecurityApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
配置application.yml
server:
port: 8080
spring:
mvc:
view:
prefix: /pages/
suffix: .jsp
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/security_authority
username: root
password: 000
mybatis:
type-aliases-package: com.jsu.entity
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.jsu: debug
创建POJO
public class SysRole implements GrantedAuthority {
private Integer id;
private String roleName;
private String roleDesc;
/**
JsonIgnore 表示该字段不参与json的转换
*/
@JsonIgnore
@Override
public String getAuthority() {
return roleName;
}
public SysRole() {
}
public SysRole(Integer id, String roleName, String roleDesc) {
this.id = id;
this.roleName = roleName;
this.roleDesc = roleDesc;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getRoleDesc() {
return roleDesc;
}
public void setRoleDesc(String roleDesc) {
this.roleDesc = roleDesc;
}
}
public class SysUser implements UserDetails {
/**
* 加入自定义属性
*/
private Integer id;
private String username;
private String password;
private Integer status;
private List<SysRole> roles;
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
public SysUser(Integer id, String username, String password, Integer status, List<SysRole> roles) {
this.id = id;
this.username = username;
this.password = password;
this.status = status;
this.roles = roles;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public List<SysRole> getRoles() {
return roles;
}
public void setRoles(List<SysRole> roles) {
this.roles = roles;
}
}
Mapper配置
public interface RoleMapper extends Mapper<SysUser> {
@Select("select r.id ,r.role_name roleName,r.role_desc roleDesc from sys_role r ,sys_user_role ur where r.id = ur.rid and ur.uid = #{uid}")
public List<SysRole> findByUid(Integer uid);
}
public interface UserMapper extends Mapper<SysRole> {
@Select("select * from sys_user where username = #{username}")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "roles",column = "id",javaType = List.class,
many = @Many(select = "com.jsu.mapper.RoleMapper.findByUid"))
})
public SysUser findByName(String username);
}
Service配置
public interface IUserService extends UserDetailsService {
}
@Service
@Transactional
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return userMapper.findByName(s);
}
}
@Controller
@RequestMapping("/product")
public class ProductController {
@Secured("ROLE_PRODUCT")
@RequestMapping("/findAll")
public String findAll(){
return "product-list";
}
}
config配置
@Configuration
@EnableWebSecurity
// 使用security 内置的方式 开启
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IUserService userService;
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
};
/**
* 认证用户的来源[内存 / 数据库]
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置类中的权限 不要加前缀
// auth.inMemoryAuthentication()
// .withUser("user")
// .password("{noop}123")
// .roles("USER");
// 使用自己数据库的数据 进行认证
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
/**
* 配置springSecurity相关信息
*/
@Override
public void configure(HttpSecurity http) throws Exception {
// 释放静态资源 指定资源拦截规则 指定自定义认证页面 指定退出认证配置 csrf配置
http.authorizeRequests()
// 释放静态资源
.antMatchers("/login.jsp","/failer.jsp","/css/**","/img/**","/plugins/**")
.permitAll()
// 指定拦截器规则
.antMatchers("/**").hasAnyRole("USER","ADMIN")
.anyRequest()
.authenticated()
.and()
// 登录 映射login 跳转自定义login.jsp 成功跳转index.jsp 失败跳转failer.jsp
.formLogin()
.loginPage("/login.jsp")
.loginProcessingUrl("/login")
.successForwardUrl("/index.jsp")
.failureForwardUrl("/failer.jsp")
.permitAll()
.and()
// 退出登录 成功跳转登录页面
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.jsp")
// 清理缓存
.invalidateHttpSession(true)
.and()
.csrf()
.disable();
}
}
配置拦截器
@ControllerAdvice
public class HandlerControllerException {
@ExceptionHandler(RuntimeException.class)
public String handlerException(RuntimeException e){
if (e instanceof AccessDeniedException){
return "forward:/403.jsp";
}else {
return "forward:/500.jsp";
}
}
}