SpringSecurity安全访问
一、描述
springSecurity应该清楚
该框架是行业之中最早的一款用于实现授权与认证检测的开发框架,随后出现了shiro框架,但是这些框架原始设计没有考虑到当今前后端分离的结构
springSecurity可以直接与springboot整合
二、编写启动类
程序启动日志:
Using generated security password: fe56a27e-85fd-4d5d-8286-a251fb5fe87d
在默认情况下如果直接启动包含“spring-boot-starter-security” 依赖库的项目,则在启动时会自动出现有一个临时的密码信息(每次启动密码读不一样),对应的用户名是“user”
三、创建controller类
浏览器访问:http://localhost:8080/message/show
会跳转到http://localhost:8080/login一个登录页面
登录之后才能看到结果
在访问某些资源的时候如果用户没有登录,那么就会要求用户登录
四、添加固定用户
在application.yml文件中追加一个固定的账户
仅仅依靠配置文件进行springSecurity的定义,那么基本上是不可能的,所以最佳的做肯定要基于Bean的模式完成
五、基于Bean配置SpringSecurity
需要创建有一个专属的配置类,而这个配置类必须继承“WebSecurityConfigurerAdapter”父类
密码加密处理
所有密码在进行处理的时候都必须提供一个密码编码器(PasswordEncoder),这个编码器所给出的类型是一个接口,而实现这个接口的就有若干种不同的加密处理形式
测试加密
![在这里插入图片描述](https://img-blog.csdnimg.cn/84e981170f994e24861e93aa71faba45.png
执行结果:
配置认证信息
当前已经由application.yml的形式转为了原始的SpringSecurity的开发形式,整个配置更加灵活
六、前后端分离模块
1、按照默认情况下,SpringSecurity在进行认证检查的时候都是直接跳转到"/login"路径之中,但在前后分离模式下不可能见到这样的表单
2、配置授权访问
本配置程序类通过antMatchers()方法配置了授权访问路径,随后利用hasRole()或access()方法实现当前用户的权限认证,如果用户拥有相应权限则允许正常访问,否则跳转到错误页显示授权错误
七、返回Rest认证信息
所有认证信息以及授权错误信息都应该以REST数据的形式返回给客户端
设置认证成功
设置登录失败
设置注销
.and().logout().logoutUrl("/logout")//设置注销地址
.clearAuthentication(true) // 注销是清除认证信息
.invalidateHttpSession(true) // 销毁当前session
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override //注销成功后返回内容
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");// 响应类型为JOSN
response.setStatus(HttpServletResponse.SC_OK);// 200状态码
Map<String, Object> result = new HashMap<>();// 响应数据
result.put("status",HttpServletResponse.SC_OK);// 当前状态
result.put("message","注销成功");
result.put("principal",null);
result.put("sessionId",request.getSession().getId());
//转换
ObjectMapper mapper = new ObjectMapper();
response.getWriter().println(mapper.writeValueAsString(result));
}
})
利用curl命令发送post请求进行登录认证处理
curl -X POST -d "uname=admin&upass=123456" "http://localhost:80/login"
执行结果
传递cookie访问路径
curl -X GET -b "JSESSIONID=274C89C0FBDCDA795B67079894B13A7F" "http://localhost:80/message/show"
用户注销curl命令
curl -X GET -b "JSESSIONID=274C89C0FBDCDA795B67079894B13A7F" "http://localhost:80/logout"
执行结果
七、UserDetailsService
1、如果认证以及授权信息都希望通过程序来实现控制,那么就需要使用到UserDetailsService业务接口、对于当前不管使用哪种形式进行用户认证以及授权数据处理,那么都需要使用到“AuthenticationManagerBuilder”处理类,而在这个类中就提到了有一个使用UserDetailsService接口实例的配置方法:
public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(T userDetailsService) throws Exception {
this.defaultUserDetailsService = userDetailsService;
return (DaoAuthenticationConfigurer)this.apply(new DaoAuthenticationConfigurer(userDetailsService));
}
2、UserDetails观察下面接口:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//获取全部授权数据
String getPassword();//获取密码
String getUsername();//获取用户名
boolean isAccountNonExpired();// 该账户是否没有过期
boolean isAccountNonLocked();// 该账户是否没有锁定
boolean isCredentialsNonExpired(); // 账户的认证数据没有过期
boolean isEnabled();//该账户是否启用
}
3、UserDetails所保存的就是完整的认证信息,同时如果想要获取所有的授权信息则需要使用到GrantedAuthority接口
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
用户的每一个角色全部读通过GrantedAuthority对象实例进行保存,而后在UserDetails里面会通过一个集合来保存所有的授权的数据信息。
4、创建GrantedAuthority接口子类,主要保存用户的授权信息。
@Data
public class Role implements GrantedAuthority {
private String rid;//角色id
private String title;//名称
@Override
public String getAuthority() { //获取授权标记
return this.rid;
}
}
5、除了授权之外还需要考虑到使用认证信息,创建UserDetails接口子类
@Data
public class Member implements UserDetails {
@Id
private String mid;//用户id
private String name;//姓名
private String password;//密码
private Integer enabled;//用户是否启用(1:true,0:false)
private transient List<Role> roles;//保存全部角色
@Override//获取授权数据
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles;
}
@Override//获取用户名
public String getUsername() {
return this.mid;
}
@Override
public String getPassword() {
return this.password;
}
@Override//账后是否过期
public boolean isAccountNonExpired() {
return true;
}
@Override//账户是否锁定
public boolean isAccountNonLocked() {
return true;
}
@Override//认证是否失效
public boolean isCredentialsNonExpired() {
return true;
}
@Override//启用状态
public boolean isEnabled() {
return this.enabled == 1;
}
}
6、创建UserDetailsService接口子类,采用默认的用户名(admin),随后手工实现用户认证信息以及授权信息的配置。
@Service
//实现全部的认证与授权数据加载
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户信息不存在!");//手工抛出异常
}
Member member = new Member();
member.setMid("admin");
member.setPassword("{bcrypt}$2a$10$BS3zEKnaZ8UYQKsu2QD89uz9.4qd2ZqHKywOMVAJLVODaIk6V4Cvm");
member.setName("管理员");
member.setEnabled(1);
Role role = new Role();
role.setRid("ROLE_ADMIN");//必须使用“ROLE_”开头
role.setTitle("管理员");
Role roleUser = new Role();
roleUser.setRid("ROLE_USER");
roleUser.setTitle("用户");
member.setRoles(Arrays.asList(role,roleUser));
return member;
}
}
7、修改SecurityConfig配置类,注入UserdetailsService接口实例,并将该类实例配置到认证管理器中。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean // 如果想使用密码,则必须配置有一个密码的编码器
public PasswordEncoder getPasswordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();// 定义密码加密器
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {//进行访问配置
http.authorizeRequests()// 进行访问路径以及对应角色
.antMatchers("/admin/**").hasRole("ADMIN") //ADMIN角色可以访问
.antMatchers("/member/**").access("hasAnyRole('USER')")//USER角色可以访问
.antMatchers("/message/**").access("hasAnyRole('ADMIN') and hasAnyRole('USER')")
.anyRequest().authenticated().and() //用户认证后允许访问
.formLogin().loginProcessingUrl("/login") //登录处理路径
.usernameParameter("uname").passwordParameter("upass")// 认证的参数名称
.permitAll()
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object principal = authentication.getPrincipal();// 认证以及授权的内容
response.setContentType("application/json;charset=UTF-8");// 响应类型为JOSN
response.setStatus(HttpServletResponse.SC_OK);// 200状态码
Map<String, Object> result = new HashMap<>();// 登录成功的响应数据
result.put("status",HttpServletResponse.SC_OK);// 当前登录状态
result.put("message","登录成功");
result.put("principal",principal);
result.put("sessionId",request.getSession().getId());
//转换
ObjectMapper mapper = new ObjectMapper();
response.getWriter().println(mapper.writeValueAsString(result));
}
}).failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");// 响应类型为JOSN
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);// 401状态码
Map<String, Object> result = new HashMap<>();// 登录失败的响应数据
result.put("status",HttpServletResponse.SC_UNAUTHORIZED);// 当前登录状态
result.put("principal",null);//返回一个空的认证数据
result.put("sessionId",request.getSession().getId());
if(e instanceof LockedException) result.put("message","账户被锁定,登录失败!");
else if(e instanceof BadCredentialsException) result.put("message","用户名或密码错误,登录失败!");
else if(e instanceof DisabledException) result.put("message","账户被禁用,登录失败!");
else if(e instanceof AccountExpiredException) result.put("message","账户已过期,登录失败!");
else if(e instanceof CredentialsExpiredException) result.put("message","密码已过期,登录失败!");
else result.put("message","未知原因,导致登录失败!");
//转换
ObjectMapper mapper = new ObjectMapper();
response.getWriter().println(mapper.writeValueAsString(result));
}
})
.and().logout().logoutUrl("/logout")//设置注销地址
.clearAuthentication(true) // 注销是清除认证信息
.invalidateHttpSession(true) // 销毁当前session
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override //注销成功后返回内容
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");// 响应类型为JOSN
response.setStatus(HttpServletResponse.SC_OK);// 200状态码
Map<String, Object> result = new HashMap<>();// 响应数据
result.put("status",HttpServletResponse.SC_OK);// 当前状态
result.put("message","注销成功");
result.put("principal",null);
result.put("sessionId",request.getSession().getId());
//转换
ObjectMapper mapper = new ObjectMapper();
response.getWriter().println(mapper.writeValueAsString(result));
}
})
.and() //开启登录表单路径
.csrf().disable(); //关闭CSRF校验
}
}
八、基于数据库实现认证授权
虽然通过UserDetailsService实现了认证与授权数据的存储,但是真正的开发是一定要通过数据库完成存储的
1、编写数据库创建脚本
-- 删除数据库、
drop database if exists springsecurity;
-- 创建数据库
create database springsecurity default character set utf8;
-- 使用数据库
use springsecurity;
-- 创建用户表(mid;登录id。name:真实姓名。password:登录密码。enabled:角色名称。)
-- enabled取值有两种:启用(enabled=1)、锁定(enabled=0)
create table member(
mid varchar(50),
name varchar(50),
password varchar(68),
enabled int(1),
constraint pk_mid primary key(mid)
) engine=innodb;
-- 创建角色表(rid:角色id,也是权限授权检测的名称。title:角色名称。)
create table role (
rid varchar(50),
title varchar(50),
constraint pk_rid primary key(rid)
) engine=innodb;
-- 创建用户-角色关联表(mid:用户id。rid:角色id。)
create table member_role
(
mid varchar(50),
rid varchar(50),
constraint fk_mid foreign key (mid) references member(mid) on delete cascade,
constraint fk_rid foreign key (rid) references role(rid) on delete cascade
) engine=innodb;
-- 添加用户数据(admin/123456、ran/123456)
insert into member values ('admin','张三','{bcrypt}$2a$10$BS3zEKnaZ8UYQKsu2QD89uz9.4qd2ZqHKywOMVAJLVODaIk6V4Cvm',1);
insert into member values ('ran','然纵','{bcrypt}$2a$10$BS3zEKnaZ8UYQKsu2QD89uz9.4qd2ZqHKywOMVAJLVODaIk6V4Cvm',0);
insert into member values ('root','李四','{bcrypt}$2a$10$BS3zEKnaZ8UYQKsu2QD89uz9.4qd2ZqHKywOMVAJLVODaIk6V4Cvm',1);
-- 添加角色数据
insert into role values ('ROLE_ADMIN','管理员');
insert into role values ('ROLE_USER','用户');
-- 添加用户与角色数据
insert into member_role values ('admin','ROLE_ADMIN');
insert into member_role values ('admin','ROLE_USER');
insert into member_role values ('ran','ROLE_ADMIN');
insert into member_role values ('root','ROLE_USER');
-- 提交事务
commit;
2、数据库信息
3、引入jpa相关依赖包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
</dependencies>
4、修改Member类,追加Spring Data JPA注解
@Data
@Entity
@Table // 表名称与类名相同,可以写简单注解
@AllArgsConstructor
@NoArgsConstructor
public class Member implements UserDetails {
@Id
private String mid;//用户id
private String name;//姓名
private String password;//密码
private Integer enabled;//用户是否启用(1:true,0:false)
@ManyToMany(targetEntity = Role.class) // 启用延迟加载
@JoinTable(name = "member_role", // 关系表名称
joinColumns = {@JoinColumn(name = "mid")}, //关联字段
inverseJoinColumns = {@JoinColumn(name = "rid")})
@JsonBackReference // jackson防止数据递归处理
private List<Role> roles;//保存全部角色
@Override//获取授权数据
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles;
}
@Override//获取用户名
public String getUsername() {
return this.mid;
}
@Override
public String getPassword() {
return this.password;
}
@Override//账后是否过期
public boolean isAccountNonExpired() {
return true;
}
@Override//账户是否锁定
public boolean isAccountNonLocked() {
return true;
}
@Override//认证是否失效
public boolean isCredentialsNonExpired() {
return true;
}
@Override//启用状态
public boolean isEnabled() {
return this.enabled == 1;
}
}
5、修改Role类,追加Spring Data JPA注解
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table
@Entity
public class Role implements GrantedAuthority {
@Id
private String rid;
private String title;
@ManyToMany(mappedBy="roles") // 多对多关联
@JsonBackReference // Jackson防止数据递归处理
private List<Member> members;
@Override
public String getAuthority() {//获取授权标记
return this.rid;
}
}
6、创建IMemberDAO接口
public interface IMemberDAO extends JpaRepository<Member,String> {
}
7、修改application.yml文件,配置druid数据源以及jpa
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springsecurity
username: root
password: 123456
jpa:
show-sql: true
properties:
hibernate:
enable_lazy_load_no_trans: true #配置延迟加载
8、修改UserDetailsServiceImpl程序类,通过DAO接口实现数据查询
@Service
//实现全部的认证与授权数据加载
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IMemberDAO iMemberDAO;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optional = this.iMemberDAO.findById(username);
if (!optional.isPresent()) throw new UsernameNotFoundException("用户信息不存在!");
return optional.get();
}
}
9、修改程序启动类,追加JPA扫描配置注解
@SpringBootApplication
@EnableJpaRepositories("com.dz.dao")
@EntityScan("com.dz.vo")
public class StartSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(StartSecurityApplication.class,args);
}
}
此时用户登录时就可以通过IMemberDAO数据层接口实现基于数据库登录认证,对于开发者来讲只需要将数据以UserDetails接口实例的形式返回,就会由Spring Security自动实现后续的判断处理。