1.入门案例
-
创建Spring Boot项目
-
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.1.7.RELEASE</version> </dependency>
-
书写请求接口
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello Stream"; } }
-
启动并访问接口,会自动跳转至如下界面,该界面为Spring Security提供
登录用户名默认为user
,密码启动时会打印在控制台,如下图所示
-
配置用户名和密码
properties.properties
spring.security.user.name=admin
spring.security.user.password=admin123
上述配置完成后,启动时,登录名及密码为admin
和admin123
2.基于内存认证
- 实现简单认证功能
平时使用时可以自己继承自WebSecurityConfigurerAdapter
,从而实现自定义配置
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("admin123")
.roles("ADMIN", "USER")
.and()
.withUser("zhangsan")
.password("123")
.roles("USER");
}
说明:
(1) passwordEncoder: 在Spring Security 5.x中引入了多种密码加密方式,开发者必须指定一种,如果不指定则访问时会报如下错误
(2) 重写configure(auth):该方法主要定义用户及其它权限,主要说明如下表
api | 用途 |
---|---|
withUser | 定义用户名 |
password | 定义用户密码 |
roles | 定义用户权限(admin:表示管理员,user:表示普通用户) |
- 自定义资源认证保护
@Override
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')")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf()
.disable();
}
api说明:
api | 说明 |
---|---|
antMatchers | 匹配请求路径 |
hasRole | 表示访问路径需要的权 限 |
hasAnyRole | 表示任意一个权限即可 |
access | 表示多个权限组合 |
anyRequest | 表示除了前面定义的URL模式之外,用户必须登录后访问 |
authenticated | 表示开启单点登录 |
formLogin | 表示提供表单登录页面 |
loginProcessingUrl | 表示登录请求接口 |
permitAll | 表示和登录相关接口都不需要认证即可访问 |
csrf() disable() | 表示关闭csrf |
上述配置完成,书写测试接口如下:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello Stream";
}
@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!";
}
}
- 登录表单配置,主要配置成功及失败处理逻辑,在上述配置中继续如下配置
.loginPage("/login_page")
.usernameParameter("name")
.passwordParameter("password")
.successHandler((httpServletRequest, resp, auth) -> {
Object principal = auth.getPrincipal();
resp.setContentType("application/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((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = 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 obj = new ObjectMapper();
out.write(obj.writeValueAsString(map));
out.flush();
out.close();
})
api说明:
api | 用途 |
---|---|
loginPage | 表示自定义登录页面 地址 |
usernameParameter | 请求认证参数名 |
passwordParameter | 请求认证参数名 |
successHandler | 表示成功逻辑 |
failureHandler | 表示失败逻辑 |
- 注销登录配置
.and()
.logout()
.logoutUrl("/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.addLogoutHandler((req, resp, auth) -> {
})
.logoutSuccessHandler(
(request, response, authentication) -> response.sendRedirect("/login_page"))
api | 说明 |
---|---|
logout | 开启注销登录配置 |
logoutUrl | 注销url |
clearAuthentication | 表示清除认证信息,默认true |
invalidateHttpSession | 表示是否使Session失效,默认true |
addLogoutHandler | 可完成一些清除工作 |
logoutSuccessHandler | 退除成功后的操作 |
- 密码加密
在spring Boot中配置密码只需要修改上下配置的PasswordEncoder即可
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
平时使用时,在注册用户时对密码进行加密,主要通过如下方式
@GetMapping("/save/user")
public int reg(String username,String password){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encodePassword = encoder.encode(password);
return userService.saveUser(username, encodePassword);
}
3.基于数据库的认证
- 设计数据库
(1) user表
create table user
(
id int auto_increment
primary key,
username varchar(32) null,
password varchar(255) null,
enabled tinyint(1) null,
locked tinyint(1) null
);
INSERT INTO study.user (id, username, password, enabled, locked) VALUES (1, 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', 1, 0);
INSERT INTO study.user (id, username, password, enabled, locked) VALUES (2, 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', 1, 0);
INSERT INTO study.user (id, username, password, enabled, locked) VALUES (3, 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', 1, 0);
(2) role表
create table role
(
id int auto_increment
primary key,
name varchar(32) null,
nameZh varchar(32) null
);
INSERT INTO study.role (id, name, nameZh) VALUES (1, 'ROLE_dba', '数据库管理员');
INSERT INTO study.role (id, name, nameZh) VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO study.role (id, name, nameZh) VALUES (3, 'ROLE_user', '用户');
(3) user_role表
create table user_role
(
id int auto_increment
primary key,
uid int null,
rid int null
);
INSERT INTO study.user_role (id, uid, rid) VALUES (1, 1, 1);
INSERT INTO study.user_role (id, uid, rid) VALUES (2, 1, 2);
INSERT INTO study.user_role (id, uid, rid) VALUES (3, 2, 2);
INSERT INTO study.user_role (id, uid, rid) VALUES (4, 3, 3);
- 引入数据库相关依赖
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
- 书写配置
application.properties
数据库配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/study
# 配置mapper文件及对应实体位置
mybatis.mapper-locations=classpath:/mapper/*Mapper.xml
mybatis.type-aliases-package=com.study.secturity.dao.entity
- 创建实体
@Data
public class MyUserDetail implements UserDetails {
private User user;
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 user.getPassword();
}
//获取当前用户对象的用户名
@Override
public String getUsername() {
return user.getUsername();
}
//当前账户是否过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//当前账户是否锁定
@Override
public boolean isAccountNonLocked() {
return Objects.equals(user.getLocked(), (byte) 0) ? true : false;
}
//当前账户是否过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//当前账户是否可用
@Override
public boolean isEnabled() {
return Objects.equals(user.getEnabled(), (byte) 1) ? true : false;
}
}
代码说明
方法 | 作用 |
---|---|
getAuthorities | 获取当前用户对象所具有的角色信息 |
getPassword | 获取当前用户对象密码 |
getUsername | 获取当前用户对象用户名 |
isAccountNonExpired | 当前账户是否过期 |
isAccountNonLocked | 当前账户是否锁定 |
isCredentialsNonExpired | 当前密码是否过期 |
isEnabled | 当前账户是否可用 |
- 创建userService
@Service
public class UserService implements UserDetailsService{
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if(Objects.isNull(user)){
throw new UsernameNotFoundException("账户不存在!");
}
MyUserDetail myUserDetail = new MyUserDetail();
myUserDetail.setUser(user);
myUserDetail.setRoles(userMapper.getUserRolesByUid(user.getId()));
return myUserDetail;
}
}
- 配置SpringSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
UserService userService;
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("admin")
.antMatchers("/db/**")
.hasRole("dba")
.antMatchers("/user/**")
.hasRole("user")
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf()
.disable();
}
上述验证流程说明:
(1) 启动程序时,加载配置文件webSecurityConfig
,确定密码加密方式,获取用户信息userService
等
(2) 当用户请求配置的访问路径时,则会先根据传入的登录用户,查询数据库权限,如果该权限有访问该路径权限则可以正常访问,如果没有权限,则会报出403
错误码
4.动态配置权限
- 数据库设计
(1)创建menu主要存放请求路径
create table menu
(
id int auto_increment
primary key,
parttern varchar(64) null
);
INSERT INTO study.menu (id, parttern) VALUES (1, '/db/**');
INSERT INTO study.menu (id, parttern) VALUES (2, '/admin/**');
INSERT INTO study.menu (id, parttern) VALUES (3, '/user/**');
(2)创建menu_role主要存放请求路径与角色关系
create table menu_role
(
id int auto_increment
primary key,
mid int null,
rid int null
);
INSERT INTO study.menu_role (id, mid, rid) VALUES (1, 1, 1);
INSERT INTO study.menu_role (id, mid, rid) VALUES (2, 2, 2);
INSERT INTO study.menu_role (id, mid, rid) VALUES (3, 3, 3);
- 自定义
FilterInvocationSecurityMetadataSource
@Component
public class MyMetadataSource implements FilterInvocationSecurityMetadataSource {
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Resource
MenuMapper menuMapper;
@Override
public Collection<ConfigAttribute> getAttributes(Object obj) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) obj).getRequestUrl();
List<Menu> allMenus = menuMapper.getAllMenus();
for (Menu menu : allMenus) {
if(antPathMatcher.match(menu.getPattern(),requestUrl)){
List<Role> roles = menu.getRoles();
String[] roleArr = new String[roles.size()];
for (int i = 0; i < roleArr.length; i++) {
roleArr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(roleArr);
}
}
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
主要实现读取权限与请求路径关系配置,保存至一个集合中
- 自定义
AccessDecisionManager
@Component
public class MyAccessDecisionMagger implements AccessDecisionManager {
@Override
public void decide(Authentication auth, Object object,
Collection<ConfigAttribute> ca)
throws AccessDeniedException, InsufficientAuthenticationException {
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
for (ConfigAttribute configAttribute : ca) {
if ("ROLE_LOGIN".equals(configAttribute.getAttribute())
&& auth instanceof UsernamePasswordAuthenticationToken) {
return;
}
for (GrantedAuthority authority : authorities) {
if (configAttribute.getAttribute().equals(authority.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("权限不足");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
- 配置
webSecurityConfig
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
UserService userService;
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(cfisms());
object.setAccessDecisionManager(cadm());
return object;
}
})
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf()
.disable();
}
@Bean
MyAccessDecisionMagger cadm() {
return new MyAccessDecisionMagger();
}
@Bean
MyMetadataSource cfisms() {
return new MyMetadataSource();
}
}
其中还包含mapper,和实体如下
mapper.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="com.study.secturity.dao.mapper.MenuMapper">
<select
id="getAllMenus"
resultMap="BaseResultMap">
select m.*,r.id as rid,r.name as rname,r.nameZh as rnamZh from menu m left join
menu_role mr on m.id = mr.mid left join role r on mr.rid = r.id
</select>
<resultMap id="BaseResultMap" type="com.study.secturity.dao.entity.Menu">
<id property="id" column="id"/>
<result property="pattern" column="parttern"/>
<collection property="roles" ofType="com.study.secturity.dao.entity.Role">
<id property="id" column="rid"/>
<result property="name" column="rname"/>
<result property="nameZh" column="rnamZh"/>
</collection>
</resultMap>
</mapper>
实体类
@Data
public class Menu{
/**主键*/
private int id;
/**请求路径*/
private String pattern;
private List<Role> roles;
}
经过上边配置,则可实现数据库配置,从而实现动态配置权限。