Spring Boot 安全管理 --Spring Security
Spring Boot 安全管理
Spring Security与Shiro简介
Java开发常用的安全框架有Shiro和Spring Security。Shiro是一个轻量级的安全管理框架,用于认证、授权、会话管理、密码管理、缓存管理等功能,Spring Security源自于Spring家族,而且Spring Boot提供了自动化配置,可以与Spring框架无缝衔接,与Shiro相比要复杂,但是功能更加强大,权限控制更加详细。
Spring Security
POM依赖文件及数据库连接
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security
spring.data.mongodb.username=root
spring.datasource.password=123456
基础配置
Spring Boot为Spring Security提供了自动化配置,我们只要简单开发就可以使用了。
1、创建项目,添加依赖配置
创建一个Spring Boot Web项目,添加Spring Security和Web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、创建接口
@RestController
public class SpringSecurityTestController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
3、浏览器访问localhost:8080/hello,hello接口会自动调转到Spring Security提供的登录界面:localhost:8080/login
默认的用户名为user,密码是随机生成的,在控制台中显示。
用户名、密码配置
如果想要指定用户名、密码、用户角色,需要在application.properties文件中配置
spring.security.user.name=sang
spring.security.user.password=123
spring.security.user.roles=admin
内存认证
内存认证自定义一个java类,继承WebSecurityConfigurerAdapter接口,重写configure方法。
/**
* 自定义MyWebSercurityConfig继承WebSecurityConfigurerAdapter
* 实现基于内存的认证
*/
@Configuration
public class MyWebSercurityConfig extends WebSecurityConfigurerAdapter {
// 确认密码的加密方式
@Bean
PasswordEncoder passwordEncoder() {
// 使用不对密码加密形式
return NoOpPasswordEncoder.getInstance();
}
/**
* 重写configure方法,定义三个用户和角色
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().
withUser("admin").password("123").roles("ADMIN", "USER").and()
.withUser("sang").password("123").roles("USER").and()
.withUser("root").password("123").roles("ADMIN", "DBA");
}
}
首先MyWebSercurityConfig继承了WebSecurityConfigurerAdapter接口,采用了密码不加密形式,然后重写了configure(AuthenticationManagerBuilder auth)方法,在方法中定义了三个用户信息:
账号:admin 密码:123 角色:ADMIN、USER
账号:sang 密码:123 角色:USER
账号:root 密码:123 角色:ADMIN、DBA
HttpSecurity
到此认证功能可以实现,但是受到保护的资源都是默认的,无法根据实际情况调整,如果想要实现就需要重写WebSecurityConfigurerAdapter中的configure(HttpSecurity http)方法。
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启HttpSecurity配置
http.authorizeRequests()
// 用户访问“/admin/**”模式的URL必须具备ADMIN的角色
.antMatchers("/admin/**")
.hasRole("ADMIN")
// 用户访问“/user/**”模式的URL必须具备ADMIN或USER的角色
.antMatchers("/user/**")
.access("hasAnyRole('ADMIN','USER')")
// 用户访问“/dba/**”模式的URL必须具备ADMIN和DBA的角色
.antMatchers("/db/**")
.access("hasRole('ADMIN')and hasRole('DBA')")
// 除了前面定义的URL模式外,用户访问其它的URL都需要认证成功后才能访问
.anyRequest()
.authenticated()
.and()
// 开启表单登录,即用户直接看到登录界面,同时配置登录接口为“/login”,
// 发起post登录请求,登录参数用户名必须为username,密码必须为password,
// 配置loginProcessingUrl接口主要方便Ajax或者移动端调用登录接口
.formLogin()
.loginProcessingUrl("/login")
// 和登录相关的接口都不需要认证即可访问
.permitAll()
.and()
.csrf()
.disable();
}
创建测试接口
@RestController
public class SpringSecurityTestController {
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
@GetMapping("/user/hello")
public String user() {
return "hello user";
}
@GetMapping("/db/hello")
public String dba() {
return "hello dba";
}
}
使用浏览器访问不同的接口,返回的数据也不同,这里就不再演示了。
登录表单详细配置
注:23-84行代码
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启HttpSecurity配置
http.authorizeRequests()
// 用户访问“/admin/**”模式的URL必须具备ADMIN的角色
.antMatchers("/admin/**")
.hasRole("ADMIN")
// 用户访问“/user/**”模式的URL必须具备ADMIN或USER的角色
.antMatchers("/user/**")
.access("hasAnyRole('ADMIN','USER')")
// 用户访问“/db/**”模式的URL必须具备ADMIN和DBA的角色
.antMatchers("/db/**")
.access("hasRole('ADMIN')and hasRole('DBA')")
// 除了前面定义的URL模式外,用户访问其它的URL都需要认证成功后才能访问
.anyRequest()
.authenticated()
.and()
// 开启表单登录,即用户直接看到登录界面,同时配置登录接口为“/login”,
// 发起post登录请求,登录参数用户名必须为username,密码必须为password,
.formLogin()
// 配置登录界面,然后用户访问需要授权的页面,会自动跳转到login_page页面让用户登录,
// 这里的登录页面是开发者自定义的页面,不是默认登录页面
.loginPage("/login_page")
// 配置loginProcessingUrl接口主要方便Ajax或者移动端调用登录接口
.loginProcessingUrl("/login")
// 定义用户名、密码的参数名,默认的参数名是username、password
.usernameParameter("name")
.passwordParameter("passwd")
// 定义登录成功的业务逻辑,用户登录成功后可以跳转到某个页面,
// 也可以返回json数据,此处返回json文件
.successHandler(new AuthenticationSuccessHandler() {
// onAuthenticationSuccess中的auth参数获取用户信息,
// 在登录成功后,将用户信息返回给客户端
@Override
public void onAuthenticationSuccess(HttpServletRequest req,
HttpServletResponse resp,
Authentication auth)
throws IOException, ServletException {
Object principal = auth.getPrincipal();
resp.setContentType("applicaotin/json;charset=utf-8");
PrintWriter out = resp.getWriter();
resp.setStatus(200);
Map<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("msg", principal);
ObjectMapper om = new ObjectMapper();
out.write(om.writeValueAsString(map));
out.flush();
out.close();
}
})
// 处理登录失败的业务逻辑
.failureHandler(new AuthenticationFailureHandler() {
// 登录失败会回调onAuthenticationFailure的AuthenticationException参数
// ,给用户一个登录失败的提示
@Override
public void onAuthenticationFailure(HttpServletRequest rep,
HttpServletResponse resp,
AuthenticationException e)
throws IOException, ServletException {
resp.setContentType("applicaotin/json;charset=utf-8");
PrintWriter printWriter = resp.getWriter();
resp.setStatus(401);
Map<String, Object> map = new HashMap<>();
map.put("status", 401);
if (e instanceof LockedException) {
map.put("msg", "账号被锁定,登录失败!");
} else if (e instanceof BadCredentialsException) {
map.put("msg", "账户名或密码输入错误,登录失败!");
} else if (e instanceof DisabledException) {
map.put("msg", "账户被禁用,登录失败!");
} else if (e instanceof AccountExpiredException) {
map.put("msg", "账户已过期,登录失败!");
} else if (e instanceof CredentialsExpiredException) {
map.put("msg", "密码已过期,登录失败!");
} else {
map.put("msg", "登录失败!");
}
ObjectMapper objectMapper = new ObjectMapper();
printWriter.write(objectMapper.writeValueAsString(map));
printWriter.flush();
printWriter.close();
}
})
// 和登录相关的接口都不需要认证即可访问
.permitAll()
.and()
.csrf()
.disable();
}
测试方法如下:(使用Postman测试接口)
注销登录配置
.and()
// 注销配置
.logout()
// 注销登录的URL请求设置
.logoutUrl("logout")
// 清除身份认证信息
.clearAuthentication(true)
// 使Session失效
.invalidateHttpSession(true)
// 开发者可以在addLogoutHandler配置数据清除工作,如Cookoe
.addLogoutHandler(new LogoutHandler() {
@Override
public void logout(HttpServletRequest rep,
HttpServletResponse resp,
Authentication authentication) {
}
})
// 注销成功后的业务逻辑配置
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest rep,
HttpServletResponse resp,
Authentication authentication)
throws IOException, ServletException {
// 返回json数据或者页面
resp.sendRedirect("/login_page");
}
})
.and()
多个HttpSecurity
如果创建多个HttpSecurity,父类HttpSercurityS 不需要继承WebSecurityConfigurerAdapter,而是在HttpSercurityS类中创建静态内部子类去继承WebSecurityConfigurerAdapter,并且在子类上添加@Configuration和@Order注解
注:
@Order(1):配置优先级,数字越小优先级越大,未加注解的优先级最小
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class HttpSercurityS {
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().
withUser("admin").password("123").roles("ADMIN", "USER").and()
.withUser("sang").password("123").roles("USER").and()
.withUser("root").password("123").roles("ADMIN", "DBA");
}
@Configuration
@Order(1)
public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.access("hasAnyRole('ADMIN','USER')")
.antMatchers("/db/**")
.access("hasRole('ADMIN')and hasRole('DBA')");
}
}
@Configuration
public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.formLogin()
.loginProcessingUrl("/others")
.permitAll()
.and()
.csrf().
disable();
}
}
}
密码加密
为了保证用户的信息安全,开发者往往会给信息使用各种加密。密码加密一般会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中创建数字“指纹”的方法,但是仅仅使用散列函数还是不够,需要在加密过程中添加“盐”,这里的盐是一个随机数,加盐后即使密码明文相同的用户生成的密码,密文也不同,这大大提高了用户信息的安全。
Spring Security提供了多种加密方案,官方推荐使用BCryptPasswordEncoder,BCryptPasswordEncoder使用BCrypt强哈希函数,开发者在使用时可以选择strength和SecureRandom实例。strength越大,密钥迭代次数越多,密钥迭代次数为2^strength。strength取值在4-31之间,默认为10。
使用BCrypt加密算法步骤如下:
package com.example.demo.config;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* BCrypt哈希加密获取
*/
public class GetBCryptPasswordEncoder {
public static void main(String[] args) {
// 密码明文
String password = "123456";
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String hashedPassword = passwordEncoder.encode(password);
System.out.println(hashedPassword);
}
}
密文:
修改passwordEncoder的加密方式:
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
将加密后的密码替换上:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().
withUser("admin")
//password("123")
.password("$2a$10$BYGJlB7c2EqrZk68IdQr5ecRspJgvUKj0DFVPmRlRY6uZ0TCZwHwS")
.roles("ADMIN", "USER");
}
方法安全
因为Spring Security与spring无缝衔接,Spring还提供了注解的形式来灵活配置,省去了URL配置的繁琐。需要@EnableGlobalMethodSecurity去开启注解的安全配置。
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
/**
* prePostEnabled = true解锁@PreAuthorize()、@PostAuthorize()
* PreAuthorize():在方法执行前进行验证
* PostAuthorize():在方法执行后进行验证
* securedEnabled = true:解锁@Secured
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig {
}
角色权限设置:
package com.example.demo.service;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class WebSecurityConfigServices {
// 访问admin需要ADMIN角色
@Secured("ROLE_ADMIN")
public String admin() {
return "hello admin";
}
//访问dba需要ADMIN和DBA角色
@PreAuthorize("hasRole('ADMIN')and hasRole('DBA')")
public String dba() {
return "hello dba";
}
// 访问user需要ADMIN、DBA、USER三个其中一个角色
@PreAuthorize("hasAnyRole('ADMIN','DBA','USER')")
public String user() {
return "hello user";
}
}
数据库认证
sql语句:创建表,添加数据
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`nameZh` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'dba', '数据库管理员');
INSERT INTO `role` VALUES ('2', 'admin', '系统管理员');
INSERT INTO `role` VALUES ('3', 'user', '用户');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`locked` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('2', 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('3', 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '1', '2');
INSERT INTO `user_role` VALUES ('3', '2', '2');
INSERT INTO `user_role` VALUES ('4', '3', '3');
SET FOREIGN_KEY_CHECKS=1;
创建实体类:角色表、用户表
public class Role {
private Integer id;
private String name;
private String nameZh;
}
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean locked;
private List<Role> roles;
// 获取用户所具有的角色信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles
) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
//获取用户密码
@Override
public String getPassword() {
return password;
}
//获取用户名
@Override
public String getUsername() {
return username;
}
//当前账户是否为过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//当前账户是否未锁定
@Override
public boolean isAccountNonLocked() {
return !locked;
}
//当前账户密码是否未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//当前账户是否可用
@Override
public boolean isEnabled() {
return enabled;
}
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 Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Boolean getLocked() {
return locked;
}
public void setLocked(Boolean locked) {
this.locked = locked;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
```java
package com.example.demo.service;
import com.example.demo.Mapper.UserMapper;
import com.example.demo.bean.User;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) throw new UsernameNotFoundException("账户不存在!");
user.setRoles(userMapper.getUserRolesByUid(user.getId()));
return user;
}
}
<?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.example.demo.Mapper.UserMapper">
<select id="loadUserByUsername" resultType="com.example.demo.bean.User">
select * from user where username=#{username}
</select>
<select id="getUserRolesByUid" resultType="com.example.demo.bean.Role">
select * from role r,user_role ur where r.id=ur.rid and ur.uid=#{id}
</select>
</mapper>
## 备注
本人在学习中整理的文档,仅供参考。如有错误请留言指出,谢谢。