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。
因此,一般来说,常见的安全管理技术栈的组合是这样的(推荐):
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-configartifactId>
<version>5.3.2.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-taglibsartifactId>
<version>5.3.2.RELEASEversion>
dependency>
<filter>
<filter-name>springSecurityFilterChainfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
filter>
<filter-mapping>
<filter-name>springSecurityFilterChainfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
<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">
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<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>
<import resource="classpath: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"/>
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
<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(Cross-site request forgery) 跨站请求伪造,是一种难以防范的网络攻击方法
SpringSecurity的csrf机制将请求方式分为两类处理
第一类:“GET”,“HEAD”,“TRACE”,"OPTIONS"四类请求可以直接通过
第二类:除了上面四类,包括POST都要被验证携带token才可以通过
方法一:直接禁用csrf,不推荐
方法二:在认证页面携带token请求
<security:http auto-config="true" use-expressions="true">
<security:csrf disabled="true"/>
security:http>
<%-- 在头部加上 --%>
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
<%-- form表单中加 --%>
<security:csrfInput/>
在这里插入图片描述
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
<security:password-encoder ref="passwordEncoder"/>
security:authentication-provider>
security:authentication-manager>
// 继承UserDetailsService
public interface UserService extends UserDetailsService {
}
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;
}
}
}
在开发中有些用户 会被禁用 但是不会删除数据库
@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;
}
}
重!!!name=“remember-me” value=“true”/“1”/“on”/"yes"都可以
<security:http auto-config="true" use-expressions="true">
<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
<security:remember-me
data-source-ref="dataSource"
remember-me-parameter="remember-me"
token-validity-seconds="60"/>
或者
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-boot.version>2.1.3.RELEASEspring-boot.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
<version>2.1.5version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
dependency>
<dependency>
<groupId>org.apache.tomcat.embedgroupId>
<artifactId>tomcat-embed-jasperartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.3.0.RELEASEversion>
<configuration>
<mainClass>com.jsu.SpringSecurityApplicationmainClass>
configuration>
<executions>
<execution>
<id>repackageid>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
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
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;
}
}
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);
}
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";
}
}
@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";
}
}
}
er.jsp")
.permitAll()
.and()
// 退出登录 成功跳转登录页面
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.jsp")
// 清理缓存
.invalidateHttpSession(true)
.and()
.csrf()
.disable();
}
}
## 配置拦截器
````java
@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";
}
}
}