一.登录security
1.自定义用户账号和密码
step01-先导入两个依赖
方式一:在核心配置文件下设定信息(不合理)
spring.security.user.password=123
spring.security.user.name=pan
方式二:根据数据库数据进行登录验证(UserDetails)(重要)
step01-mysql的连接
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql:///test02?serverTimezone=Asia/Shanghai
step02-实体类实现UserDetails(是SpringSecurity中定义的用户定义规范)
//获取当前用户的权限/角色
Collection<? extends GrantedAuthority> getAuthorities();
//获取用户密码
String getPassword();
//如果SpringSecurity框架想要获取用户名就会调用改方法,返回的变量名叫什么都行
String getUsername();
//判断当前用户账户是否没有过期,正常来说在数据库中,有一个字段可以显示该账户状态
boolean isAccountNonExpired();
//当前用户是否没有锁定
boolean isAccountNonLocked();
//账户密码是否没有过期
boolean isCredentialsNonExpired();
//当前用户是否可用,正常来说在数据库中,有一个字段可以显示该账户状态
boolean isEnabled();
public class User implements UserDetails {
private Integer id;
private String username;
private String address;
private String password;
private String favorites;
private Integer gender;
private String grade;
private Integer age;
private boolean enabled;
/**
* 获取当前用户的角色
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* 获取用户密码
* @return
*/
@Override
public String getPassword() {
return password;
}
/**
* 如果 Spring Security 框架需要获取到用户名,他就会调用这个方法
*
* 所以,该方法返回值的变量名称,实际上无所谓,变成名称可以是任何名字
*
* @return
*/
@Override
public String getUsername() {
return username;
}
/**
* 判断当前账户是否过期,正常来说,应该在数据库中,有一个账户是否过期的字段,在这里直接返回即可
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账户是否没有锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 账户密码是否没有过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 账户是否可用
* @return
*/
@Override
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setUsername(String username) {
this.username = username;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setPassword(String password) {
this.password = password;
}
public String getFavorites() {
return favorites;
}
public void setFavorites(String favorites) {
this.favorites = favorites;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public String getGrade() {
return grade;
}
public void setGrade(String grade) {
this.grade = grade;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username=" + username +
", address=" + address +
", password=" + password +
", favorites=" + favorites +
", gender=" + gender +
", grade=" + grade +
", age=" + age +
"}";
}
}
第二步:在实现service层实现UserDetailsService,在loadUserByUsername中判断前端与数据库中的值是否一致,和shiro的Realm相似
@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.getRolesByUid(user.getId()));
//返回给User类中,因为该类实现UserDetails的方法中的很多方法,这些方法是将值给SpringSecurity
return user;
}
}
2.自制登录页面(SecurityFilterChain)
第一步:需要设置一个登录界面,还有在Controller层输写一个登录界面的接口
第二步:配置类,改登录界面的路径
注意:security底层就是使用过滤器,一共有16个过滤器,称为过滤器链;
1.spring5以前,springboot2.7.7
@Configuration
public class SecurityConfig {
/**
*spring5之前配置spring security的过滤器链
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
//开始过滤器的配置
.authorizeRequests()
//拦截所有请求,这个就相当于 /**
.anyRequest()
//表示拦截下来的请求,都必须认证(登录)之后才能访问
.authenticated()
.and()
//开始登录表单的配置,其实配置的是 UsernamePasswordAuthenticationFilter 过滤器
.formLogin()
//首先指定登录页面的地址
//登录页面,默认是 /login GET 请求
.loginPage("/login")
//指定登录接口的地址
//默认情况下,登录接口也是 /login POST 请求
.loginProcessingUrl("/doLogin")
//配置登录的用户名的 key,默认就是 username
.usernameParameter("username")
//配置登录的用户密码的 key,默认就是 password
.passwordParameter("password")
//登录成功之后的跳转页面,这个是服务端跳转,请求转发
// .successForwardUrl("/hello")
//登录成功之后的跳转页面,这个是客户端跳转,重定向
//假设一开始访问 /a,系统自动跳转到登录页面,登录成功之后,就会跳转回 /a,就是你想访问哪个网页的,等你登录后,直接访问该网页
//假设一开始就访问登录页面,那么登录成功之后,才会来到 /hello
.defaultSuccessUrl("/hello")
//允许登录表单的路径访问得到
.permitAll()
.and()
//禁用 csrf 保存策略,本质上就是从 Security 过滤器链中移除 CsrfFilter 过滤器
.csrf()
.disable();
return http.build();
//第一个参数是拦截路径,第二个参数是写过滤器,这里没有写过滤器
//这个配置表示拦截所有的请求,但是拦截下来的之后,不经过任意过滤器
// return new DefaultSecurityFilterChain(new AntPathRequestMatcher("/**"), new ArrayList<>());
}
}
2.spring6之后,springboot3.0.1
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//这个配置表示拦截所有请求,但是拦截下来之后,不经过任何过滤器
// return new DefaultSecurityFilterChain(new AntPathRequestMatcher("/**"));
http.authorizeHttpRequests()
// /login.css 这个地址,不需要登录就可以访问
// .requestMatchers("/login.css").permitAll()
.requestMatchers("/hello").permitAll()
//拦截所有请求
.requestMatchers("/**")
//所有请求都必须登录之后才可以访问
.authenticated()
.and()
//表示禁用 csrf 防御机制,这个禁用的本质就是将 CsrfFilter 从过滤器链中移除掉
.csrf().disable()
//开启表单登录,如果没有自己去配置 SecurityFilterChain,默认表单登录是开启的,但是如果自己配置了,则默认的表单登录就会被覆盖掉,所以要重新配置
.formLogin()
//配置登录接口,登录接口是 POST 请求,这个地方,如果不配置,默认地址是 /login
.loginProcessingUrl("/doLogin")
//配置登录用户名的 key,默认就是 username
.usernameParameter("name")
//配置登陆密码的 key,默认是 password
.passwordParameter("passwd")
//配置登录页面,默认登录页面地址是 /login(GET 请求)
.loginPage("/login.html")
//登录成功之后的跳转页面,这个跳转方式是请求转发(服务端跳转)
//一般不用这种
// .successForwardUrl()
//默认登录成功之后的跳转页面,这个是重定向跳转
//如果用户首先访问 /a,但是因为没有登录,自动跳转到登录页面,登录成功之后,就会跳转回 /a
//如果用户一开始直接就访问的是登录页面,那么登录成功之后,就会跳转到 /hello
// .defaultSuccessUrl("/hello")
//登录成功的处理器
//auth 表示当前登录成功的用户对象
.successHandler((req, resp, auth) -> {
//获取当前登录成功的用户对象
User user = (User) auth.getPrincipal();
user.setPassword(null);
resp.setContentType("application/json;charset=utf-8");
Map<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("message", "登录成功");
map.put("data", user);
resp.getWriter().write(new ObjectMapper().writeValueAsString(map));
})
//失败之后的跳转路径(请求转发)
// .failureForwardUrl()
//失败路径
// .failureUrl()
//配置失败的处理器
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
Map<String, Object> map = new HashMap<>();
map.put("status", 500);
map.put("message", "登录失败");
if (e instanceof BadCredentialsException) {
map.put("message", "用户名或者密码输入错误,登录失败");
} else if (e instanceof AccountExpiredException) {
map.put("message", "账户过期,登录失败");
} else if (e instanceof CredentialsExpiredException) {
map.put("message", "密码过期,登录失败");
} else if (e instanceof DisabledException) {
map.put("message", "账户被禁用,登录失败");
} else if (e instanceof LockedException) {
map.put("message", "账户被锁定,登录失败");
}
resp.getWriter().write(new ObjectMapper().writeValueAsString(map));
})
.permitAll();
return http.build();
}
3.放行策略
方法一:配置过滤器链(一般使用接口放行使用)
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
//开始过滤器的配置
.authorizeRequests()
//允许/css/login.css匿名访问,就是给静态资源直接访问,不用拦截的,不能在anyrequest之后执行
.antMatchers("/css/login.css")
//允许所有用户
.permitAll()
//允许匿名用户访问,不允许登录用户访问,所以我们正常使用permitall
//.anonymous()
//拦截所有请求,这个就相当于 /**
.anyRequest()
//表示拦截下来的请求,都必须认证(登录)之后才能访问
.authenticated()
方法二:配置一个WebSecurityCustomizer接口进行放行(一般静态资源选择用此放行规则)
/**
*spring6之后springboot3.0.1
* 资源不用拦截,直接放行,有两种思路:
*
* 1. web.ignoring().requestMatchers("/login.css"); 这个放行方式,本质上。请求将不再经过 Spring Security 过滤器链
* 2. .requestMatchers("/login.css").permitAll() 这个放行方式,请求还是会经过 Spring Security 过滤器链,但是不会被拦截
*
* 如果要放行的资源,是静态资源,不需要进行 Java 运算的,例如 HTML/CSS/JS/mp3/mp4/图片,那么可以使用第一种放行方式
* 如果是一个 Java 接口,则建议使用第二种放行方式。
* @return
*/
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
//这个也是给资源放行
web.ignoring().requestMatchers("/login.css");
}
};
}
//spring5之前springboot2.7.7
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
web.ignoring().antMatchers("css/login.css");
}
};
}
注意:如果是静态资源就选择用第二种方式用接口WebSecurityCustomizer进行,但是如果是接口的放行那么就用第一种方式,因为当登录的时候,我们希望请求可以经过SpringSecurity的SecurityContextPersistenceFilter,这个过滤器将会从httpSession中获得登录用户的对象,然后存到SecurityContextHolder里面去,然后SecurityContextHolder本质上就是存储到ThreadLocal里面,保持线程一致性
4.登录的时候获取登录对象(SecurityContextHolder.getContext().getAuthentication())
1.在controller中获取登录对象
方式一:通过SecurityContextHolder来获取
@RestController
//@RequestMapping("/user")
public class UserController {
@GetMapping("/hello")
public String hello(HttpSession session) {
// 获取当前登录成功的登录信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 获得登录用户的名称
String name = authentication.getName();
//从httpSession里面获取登录对象,一般不这样做,因为我们还是希望可以在同一个线程的
//SecurityContext spring_security_context = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");
//String name1 = spring_security_context.getAuthentication().getName();
//System.out.println("name1 = " + name1);
return "hello:" + name;
}
}
方式二:通过参数Principal
@GetMapping("/hello")
public String hello(Principal principal) {
//获取当前登录成功的用户名
String name = principal.getName();
System.out.println("name = " + name);
return "hello";
}
方式三:通过参数HttpSession
2 .在service中获取登录的对象
方式一:通过SecurityContextHolder
@Service
public class HelloService {
public void hello() {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
//获取当前登录成功的用户对象
User user = (User) authentication.getPrincipal();
System.out.println("user = " + user);
}
}
方式二:RequestContextHolder.getRequestAttributes()).getRequest()来获取request来获取session
@Service
public class HelloService {
public void hello() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
new Thread(new Runnable() {
@Override
public void run() {
//获取当前请求对象
SecurityContext sc = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
Authentication authentication1 = sc.getAuthentication();
User user = (User) authentication1.getPrincipal();
System.out.println("fffffffff = " + user);
}
}).start();
}
}
5.通过Json来进行登录验证
引言:springsecurity是通过key-value的形式获取登录的账户和密码的
方式一:重写UsernamePasswordAuthenticationFilter类里的attemptAuthentication方法
step01-重写attemptAuthentication方法
public class MyJsonFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
//此时就认为请求参数是 JSON 形式的
User user = null;
try {
user = new ObjectMapper().readValue(request.getInputStream(), User.class);
} catch (IOException e) {
e.printStackTrace();
}
String username = user.getUsername();
username = (username != null) ? username.trim() : "";
String password = user.getPassword();
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
return super.attemptAuthentication(request, response);
}
}
}
step02-注入到spring容器中
@Configuration
public class SecurityConfig {
@Autowired
UserService userService;
/**
* 这个过滤器,替代的是 UsernamePasswordAuthenticationFilter,所以我们之前关于表单的配置,现在都要配置给 MyJsonFilter 才会生效
*
* @return
*/
@Bean
MyJsonFilter myJsonFilter() {
MyJsonFilter myJsonFilter = new MyJsonFilter();
// myJsonFilter.setFilterProcessesUrl();
// myJsonFilter.setUsernameParameter();
// myJsonFilter.setPasswordParameter();
myJsonFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
resp.getWriter().write("success");
});
myJsonFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
}
});
myJsonFilter.setAuthenticationManager(authenticationManager());
return myJsonFilter;
}
/**
* 这个是一个认证管理器
* @return
*/
@Bean
AuthenticationManager authenticationManager() {
//DaoAuthenticationProvider 这个对象负责数据库密码的校验
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userService);
ProviderManager manager = new ProviderManager(daoAuthenticationProvider);
return manager;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.authenticated()
.and()
.csrf().disable();
http.formLogin()
//如果只配置登录页面,那么默认情况下,登录接口也是 /login.html
.permitAll();
//新加一个过滤器进来,将之放到 UsernamePasswordAuthenticationFilter 过滤器所在的位置
http.addFilterAt(myJsonFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
方式二:通过将json的值直接放入到UsernamePasswordAuthenticationToken中,
step01-首先在配置类中获取AuthenticationManager
@Configuration
public class SecurityConfig {
//获取的是登录成功的用户名
@Autowired
UserService userService;
//和shiro中的安全管理器DefaultWebSecurityManager相似,UserDetailsService相当于shiro的Realm
/**
* 这个是一个认证管理器
* @return
*/
@Bean
AuthenticationManager authenticationManager() {
//调用AuthenticationProvider的子类DaoAuthenticationProvider来验证。
//默认实例化AuthenticationProvider的一个实现:DaoAuthenticationProvider。DaoAuthenticationProvider通过接口UserDetailsService的实现类从内存或DB中获取用户信息UserDetails(UserDetails十分类似Authentication,也是一个接口,但是与Authentication用途不同,不要搞混)。DaoAuthenticationProvider通过函数authenticate比较入参authentication与UserDetails是否相符,来判断用户是否可以登录。如果相符,会将获得的UserDetails中的信息补全到一个Authentication实现类,并将该实现类作为认证实体返回。以后便可以通过当前上下文的认证实体Authentication获取当前登录用户的信息。
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userService);
//调用AuthenticationManager的实现类ProviderManager
ProviderManager pm = new ProviderManager(provider);
return pm;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/**")
.authenticated()
.and()
.csrf().disable();
return http.build();
}
step02-接口中直接获取json
@RestController
public class LoginController {
//设定一个json的登录参数,因为底层我们是根据key-value登录的
@Autowired
AuthenticationManager authenticationManager;
@PostMapping("/doLogin")
//@RequestBody是json的参数
public String login(@RequestBody User user) {
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(),user.getPassword());
//执行认证操作
try {
//参数 authRequest 是一个未经认证的 authentication,而方法的返回值是一个认证后的 authentication
Authentication authentication = authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
return "success";
} catch (AuthenticationException e) {
e.printStackTrace();
return e.getMessage();
}
}
}
@RestController
public class LoginController {
@Autowired
AuthenticationManager authenticationManager;
@PostMapping("/login")
public Map<String, Object> login(@RequestBody User user) throws IOException {
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
try {
//这里就是执行具体的认证操作
//参数是一个未经认证的 Authentication 对象,而 authenticate 方法返回值则是一个经过认证的 Authentication 的对象
Authentication auth = authenticationManager.authenticate(token);
//将认证后的 auth 对象存入 SecurityContextHolder 中
SecurityContextHolder.getContext().setAuthentication(auth);
//获取当前登录成功的用户对象
User authUser = (User) auth.getPrincipal();
authUser.setPassword(null);
Map<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("message", "登录成功");
map.put("data", authUser);
return map;
} catch (AuthenticationException e) {
Map<String, Object> map = new HashMap<>();
map.put("status", 500);
map.put("message", "登录失败");
if (e instanceof BadCredentialsException) {
map.put("message", "用户名或者密码输入错误,登录失败");
} else if (e instanceof AccountExpiredException) {
map.put("message", "账户过期,登录失败");
} else if (e instanceof CredentialsExpiredException) {
map.put("message", "密码过期,登录失败");
} else if (e instanceof DisabledException) {
map.put("message", "账户被禁用,登录失败");
} else if (e instanceof LockedException) {
map.put("message", "账户被锁定,登录失败");
}
return map;
}
}
}
6.注销,未登录访问的处理器
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/**")
.authenticated()
.and()
.csrf().disable()
//开启注销登录的配置
.logout()
//第一个参数是访问路径,第二个是请求的方法
.logoutRequestMatcher(new AntPathRequestMatcher("/a", "POST"))
//注销成功后的回调
.logoutSuccessHandler((req, resp, auth) -> {
try {
resp.getWriter().write("/a logout success");
} catch (IOException e) {
e.printStackTrace();
}
})
//是否清除认证信息(清除 SecurityContextHolder 中的信息,默认是true)
.clearAuthentication(true)
//是否销毁 HttpSession
.invalidateHttpSession(true)
.permitAll()
.and()
//前端出现403,没有登录,禁止访问
.exceptionHandling()
//配置未认证访问的处理器
.authenticationEntryPoint(new AuthenticationEntryPoint() {
/**
* 当用户没有认证,但是却访问了一个需要认证之后才能访问的接口,那么就会触发该方法
* @param request
* @param response
* @param authException
* @throws IOException
* @throws ServletException
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("status", 500);
map.put("message", "尚未登录,请登录");
//设置UTF-8
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(new ObjectMapper().writeValueAsString(map));
}
});
return http.build();
}
7.加密(PasswordEncoder)
1.密码的加密匹配器
以下是原理
接口(PasswordEncoder)自带盐
常用的实现类:DelegatingPasswordEncoder(代理密码加密)通过PasswordEncoderFactories来调用,默认使用的是BCryptPasswordEncoder
public interface PasswordEncoder {
//将传来的密码,加盐加密生成编码,例如当你注册的时候,将明文转成密文
String encode(CharSequence rawPassword);
//判断密码是否正确,第一个参数是明文密码,第二个参数是加盐加密密码
boolean matches(CharSequence rawPassword, String encodedPassword);
//升级密码
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
注意**:1.你用什么实现类进行加盐加密的就得用什么实现类来进行比对
2.当没有配置加密匹配器的时候,就会用DelegatingPasswordEncoder密码匹配器进行调用默认的密码匹配器SCryptPasswordEncoder,因为它底层可以给其余的10种密码匹配器取名,我们只需要在数据库的密码中前面写:例如{noop}123,就是使用NoopPasswordEncode密码匹配器
//spring5+springboot2.7.7+jdk8
public final class PasswordEncoderFactories {
private PasswordEncoderFactories() {
}
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
//第一个参数,表示的是默认的密码匹配器
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}
//spring6+springboot3.0.1+jdk17
public final class PasswordEncoderFactories {
private PasswordEncoderFactories() {
}
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}
step01-配置passwordEncoder类,我们可以使用系统默认的代理密码匹配,也可以写自己想要的密码匹配器,只需要在配置类中修改 DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders) ,它会自带前缀
@Configuration
public class SecurityConfig {
/**
* 配置了这个 Bean 之后,项目中的密码加密都是用 BCryptPasswordEncoder,那么就不需要额外指定密码加密方案了
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
return passwordEncoder;
}
}
2.密码升级(UserDetailsPasswordService)
原因:当用户的密码需要实现升级的时候,也就是想要修改成别的密码匹配器时或者数据库中可能存在不同的加密匹配器的加密机制,想数据库中的密码全部升级为统一的密码加密机制时,就会用到密码升级
方式一:修改配置类的密码匹配器
步骤:step01-配置passwordEncoder类
@Configuration
public class SecurityConfig {
/**
* 配置了这个 Bean 之后,项目中的密码加密都是用 BCryptPasswordEncoder,那么就不需要额外指定密码加密方案了
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
return passwordEncoder;
}
}
step02-需要在service接口中继承UserDetailsPasswordService接口,实现UserDetailsPasswordService接口中的updatePassword方法
/**
* 当用户登录的时候,系统会自动判断用户密码是否需要升级
* @param user:登录时候的用户
* @param newPassword 新的加密后的密码
* @return
*/
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
//条件构造器
UpdateWrapper<User> uw = new UpdateWrapper<>();
//将修改为新密码
uw.lambda().set(User::getPassword, newPassword)
//是根据哪个用户id
.eq(User::getId, ((User) user).getId());
//sql修改
boolean update = update(uw);
//((User) user).setPassword(newPassword);
return user;
}
@Service
public class UserService implements UserDetailsService, UserDetailsPasswordService {
@Autowired
UserMapper userMapper;
/**
* 根据用户名称查询用户对象,当用户登录的时候,这个方法会被自动触发
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
return user;
}
/**
* 当用户每次登录的时候,都会去检查用户的密码是否需要升级,如果需要升级,则当前方法会被触发。
* 升级的情况:
* 1. 无论是系统默认配置的 DelegatingPasswordEncoder 还是开发者自己配置的 DelegatingPasswordEncoder,DelegatingPasswordEncoder 中都有一个默认的密码加密方案。那么当用户登录的时候,系统就会去检查用户当前的密码加密方案是否为 DelegatingPasswordEncoder 中默认的加密方案,如果不是,则当前方法就会被触发,系统会进行密码升级,将现有的密码加密方案改为 DelegatingPasswordEncoder 中默认的密码加密方案。
* 2. 同一种密码加密方案也可能存在升级的情况,例如 BCryptPasswordEncoder 中,可以设置密码的强度,密码强度不同,也会导致密码自动升级
* @param user:实体类的User
* @param newPassword:新的加密后的密码
* @return
*/
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
User u = (User) user;
u.setPassword(newPassword);
//sql语句:修改数据库的密码
userMapper.updatePassword(u);
return user;
}
}
step03-测试
如果数据库密码是{noop}123,就是升级为{bcrypt},系统默认的
方式二:修改密码匹配器的强度(参数)
@Bean
PasswordEncoder passwordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder(11));
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
return passwordEncoder;
}
8.remember me
1.remember me的基本使用
1.与shiro的区别是:系统重启了,remember me还是存在
2.springboot3.0.1才有算法名
step01-接口
@RestController
public class HelloController {
/**
* 这里的参数 Principal 就表示当前登录成功的用户对象
* 设置这个接口remember me或者登录都可以访问
* @param principal
* @return
*/
@GetMapping("/hello")
public String hello(Principal principal) {
return "hello";
}
/**
* 设置这个接口只有通过Rememeberme才能够访问
* @return
*/
@GetMapping("/rm")
public String rm(){
return "rm";
}
/**
* 设置是账户密码登录时才可以登录
* @return
*/
@GetMapping("/hello2")
public String hello2(){
return "密码登录的";
}
}
step02-配置过滤器链
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
//只能通过remember me才能够访问
.antMatchers("/rm").rememberMe()
//只有时账户密码登录才能够访问
.antMatchers("/hello2").fullyAuthenticated()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
.rememberMe()
//随便给一个key,以后系统重启了,remember me还是存在的,和shiro不太一样
.key("a")
.and()
.csrf()
.disable();
return http.build();
}
2.持续化令牌
将用户登录的用户名,series,token存储起来,也是通过series,token来确定有没有被别人登陆了,每当自动登录的时候,token就会改变一次,提高安全性,当注销的时候存入到数据库的信息就会删除
step01-在数据库中执行上面的sql语句,创建表persistent_logins
step02-在配置文件中重新配置PersistentTokenBasedRememberMeServices里的PersistentTokenRepository
@Configuration
public class SecurityConfig {
@Autowired
DataSource dataSource;
@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
return repository;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
// /rm 必须是以 RememberMe 的方式登录才可以访问
.requestMatchers("/rm").rememberMe()
//这个表示必须用户名密码登录,才能访问 /hello2
.requestMatchers("/hello2").fullyAuthenticated()
.requestMatchers("/**")
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
.rememberMe()
.key("123")
//配置PersistentTokenRepository
.tokenRepository(jdbcTokenRepository())
.and()
.csrf().disable();
return http.build();
}
}
9.会话管理
引言:如果在配置文件中设置了最大登录的是1个时,每一次登录的时候都会将另一个挤下去,或者登不上,是根据User实体类中的equals和hashCode来判断用户是否登录
step01-实体类User写equals和hashCode的方法
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private String address;
private Boolean enabled;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(username, user.username);
}
@Override
public int hashCode() {
return Objects.hash(username);
}
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 String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
/**
* 获取当前用户的权限/角色
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**、
* 获取用户密码
* @return
*/
@Override
public String getPassword() {
return password;
}
/**
* 获取用户名
* @return
*/
@Override
public String getUsername() {
return username;
}
/**
* 账户是否没有过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账户是否没有被锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 密码是否没有过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 账户是否可用
* @return
*/
@Override
public boolean isEnabled() {
return enabled;
}
}
step02-添加会话的配置信息
判断用户是可以同时在线几个是根据Map<User,List<Session>>
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.sessionManagement()
/**
*Map<User,List<Session>>
* 底层逻辑是有一个映射表,Map,map 的 key 是用户对象,value 则是一个集合,集合中保存了 session
*/
//设置同一个用户的 session 并发数
.maximumSessions(1)
//当达到最大并发数的时候,是否禁止新的登录
.maxSessionsPreventsLogin(true)
.and()
//防止会话攻击,(默认也是这个配置)
.sessionFixation().migrateSession()
.and()
.csrf()
.disable();
return http.build();
}
/**
* 当注销的时候,map里面还存着用户的session的,我们得通过这个bean来触发监听器
* 这是一个事件发布器,这个会自动将 HttpSession 销毁的事件发布出去,发布之后会被 map 感知到,进而移除 map 中的 session
*
* @return
*/
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
10.防止csrf攻击
起因:黑客利用浏览器存在cookie,当你登录时,然后又访问了黑客访问的页面的端口,当你访问时就可以直接盗窃你的信息,甚至转账。(小程序和软件不存在这个问题)
解决:我们可以开启csrf,当我们自己访问post,delete,put也无法访问,黑客更加无法访问了,这是我们只需要在浏览器上添加过滤器给予你的csrf字符串,你就可以访问,黑客是无法获取csrf字符串的(可以获取cookie是因为浏览器存着,然后访问时直接调用)
情况一:csrf过滤器随机生成的值,我们要以参数的形式请求上去
//提交_csrf的参数过去,就可以访问了
<form action="/transfer" method="post">
<input type="text" name="username">
<input type="text" name="money">
<input type="hidden" name="_csrf" th:value="${_csrf.token}">
<input type="submit" value="转账">
</form>
情况二:用到ajax来进行访问,将cookie里面的参数提取出来,然后发送请求上去
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- <script src="jquery-3.6.3.min.js"></script>-->
<!-- <script src="jquery.min.js"></script>
<script src="jquery.cookie.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
</head>
<body>
<input type="text" id="username" value="zhangsan">
<input type="text" id="password" value="123">
<button onclick="doTransfer()">登录</button>
<div id="result"></div>
<script>
function doTransfer() {
let token = $.cookie('XSRF-TOKEN');
console.log("token>>>>", token)
// _csrf: token:将配置中的
$.post("/login", {username: $("#username").val(), password: $("#password").val(), _csrf: token},function (msg) {
$("#result").html(msg);
});
}
</script>
</body>
</html>
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
//添加了csrf安全过滤器,因为黑客可以通过你浏览的网址获取你登录时候的cookie,从而导致你的账户被获取,你可以添加csrf自动创建的字符串,加在后面,即便黑客获取cookie,但是无法获取你的字符串也没用,因为是看你请求的参数里面有没有csrf参数的,不是看你的coken
//这个将csrf存放在cookie里面的,然后通过js来将cookie的值读取出来,找到cookie中的_csrf中的值,作为普通请求的参数携带上。服务端是在你请求的参数上找有没有_csrf的,并不是在cookie中解析寻找的
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
}
11.多个过滤器链
1.多个用户访问是不同的过滤器链
step01-接口
@RestController
public class HelloController {
/**
* 我希望,这个访问这个接口的请求,需要经过 ConcurrentSessionFilter 过滤器
* @return
*/
@GetMapping("/user/hello")
public String user() {
return "hello uesr";
}
/**
* 访问这个请求的地址,不需要经过 CsrfFilter
* @return
*/
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
step02-配置类
@Configuration
public class SecurityConfig {
@Bean
UserDetailsService us1() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("{noop}123").roles("admin").build());
return manager;
}
@Bean
UserDetailsService us2() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("lisi").password("{noop}123").roles("user").build());
return manager;
}
/**
* 这个就是配置的第一个过滤器链
* @param http
* @return
* @throws Exception
*/
@Bean
SecurityFilterChain securityFilterChain01(HttpSecurity http) throws Exception {
//这个地方是在配置过滤器链,这个 antMatcher 方法表示如果你的请求格式是 /user/** 的话,那么就会进入到当前链中
http.antMatcher("/user/**")
.authorizeRequests()
.antMatchers("/user/**").hasAuthority("user")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/user/login")
.successHandler((req, resp, auth) -> {
resp.getWriter().write("success01");
})
.failureHandler((req, resp, e) -> {
resp.getWriter().write("failure01");
})
.permitAll()
.and()
.sessionManagement()
.maximumSessions(1)
.and()
.and()
.csrf().disable()
//每一个过滤器链,可以对应不同的数据源,如果我们不为每一个过滤器链设置单独的数据源,那么也可以直接向 Spring 容器中注册一个数据源,那么这个数据源就是公用的。
.userDetailsService(us1());
return http.build();
}
/**
* 这个就是配置的第二个过滤器链
* @param http
* @return
* @throws Exception
*/
@Bean
SecurityFilterChain securityFilterChain02(HttpSecurity http) throws Exception {
//这个地方是在配置过滤器链,这个 antMatcher 方法表示如果你的请求格式是 /admin/** 的话,那么就会进入到当前链中
http.antMatcher("/admin/**")
.authorizeRequests()
.antMatchers("/admin/**").hasAuthority("admin")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/admin/login")
.successHandler((req, resp, auth) -> {
resp.getWriter().write("success02");
})
.failureHandler((req, resp, e) -> {
resp.getWriter().write("failure02");
})
.permitAll()
.and()
.csrf().disable()
.userDetailsService(us2());
return http.build();
}
@Bean
SecurityFilterChain securityFilterChain03(HttpSecurity http) throws Exception {
//任何用户都可以访问
http.antMatcher("/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
return http.build();
}
}
2.多个数据源对应的过滤器链
多个数据源
step01-配置多个数据源对应的登录UserDetailsService(这个相当于Realm,用来获取前端的username)
@Service
public class UserService2207 implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 根据用户名称查询用户对象,当用户登录的时候,这个方法会被自动触发
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
@Ds("master")
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
return user;
}
}
@Service
public class UserServiceJpaDemo implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 根据用户名称查询用户对象,当用户登录的时候,这个方法会被自动触发
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
@Ds("slave")
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
return user;
}
}
step02-配置类,给对应的service匹配对应的过滤器链
@Configuration
public class SecurityConfig {
@Autowired
UserService2207 userService2207;
@Autowired
UserServiceJpaDemo userServiceJpaDemo;
/**
* 提供一个这样的 Bean,就是提供了一个过滤器链,这个过滤器链中约 10 多个过滤器
*
* @param http
* @return
* @throws Exception
*/
@Bean
SecurityFilterChain phoneSecurityFilterChain(HttpSecurity http) throws Exception {
http
//这个表示配置当前过滤器链的拦截规则
.securityMatcher("/phone/**")
// UserDetailsService 分为两种,一种是全局的,另外一种是局部的,如果我们不给每一个过滤器链单独设置 UserDetailsService,则都使用的是全局的
.userDetailsService(userService2207)
.authorizeHttpRequests().requestMatchers("/**")
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/phone/login")
.successHandler((req,resp,auth)->{
resp.getWriter().write("phone login success");
})
.failureHandler((req,resp,e)->{
resp.getWriter().write(e.getMessage());
})
.permitAll()
.and()
.csrf().disable();
return http.build();
}
@Bean
SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
http
//这个表示配置当前过滤器链的拦截规则
.securityMatcher("/web/**")
.userDetailsService(userServiceJpaDemo)
.authorizeHttpRequests().requestMatchers("/**")
.authenticated()
.and()
.formLogin()
.successHandler((req,resp,auth)->{
resp.getWriter().write("web login success");
})
.loginPage("/web/login")
.loginProcessingUrl("/web/login")
.permitAll();
return http.build();
}
}
12.JWT登录(无状态登录)
有状态登录携带的sessionid是没有含义的,就只是随机id;无状态的token是有含义的
1.httpBasic
只需要将formLogin()写成httpBasic()就可以实现了,但是有缺点:1.注销问题,无法注销 2.续期问题,不会自动重置时间 3.密码重置 4.把密码用户信息放在请求头,不安全,每一次调用一个接口都会显示,因为是无状态,它需要无时无刻校验
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/**")
.authenticated()
.and()
.httpBasic();
return http.build();
}
}
2./jwtk/jjwt
step01-jwt专属·依赖
step02-配置类,如果成功登录就生成JWT字符串
@Configuration
public class SecurityConfig {
//可以随机是一段字符串
public static final Key KEY = Keys.hmacShaKeyFor("jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395jfkdafljksajklf75894237uafkldsjfalkj8134784395".getBytes());
//引入写好的过滤器
@Autowired
JwtFilter jwtFilter;
/**
* 配置 Spring Security 的过滤器链
*
* @return
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//因为jwtFilter中有用户名,密码,权限的设置在UsernamePasswordAuthentication该类,所以在它前面执行
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
http.authorizeHttpRequests()
.requestMatchers("/**")
.authenticated()
.and()
//关闭 session 的创建,这样就不会产生sessionid了,实现无状态访问
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.formLogin()
.successHandler((req, resp, auth) -> {
//保存的数据是需要map存储
Map<String, String> map = new HashMap<>();
//获取当前登录成功的用户角色,是在User实体类的权限角色方法中获取的
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
// ROLE_admin,ROLE_user,使用Stream流将获取的权限以,隔开
map.put("authorities", authorities.stream().map(a -> a.getAuthority()).collect(Collectors.joining(",")));
//生成 JWT 字符串
String jws = Jwts.builder()
//配置主题
.setSubject(auth.getName())
//过期时间,七天之后过期
.setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000))
//令牌签发时间
.setIssuedAt(new Date())
//这个里边可以保存比较丰富的数据类型
.setClaims(map)
//将Key放入
.signWith(KEY).compact();
resp.getWriter().write(jws);
})
.failureHandler((req, resp, e) -> {
resp.getWriter().write("login error>>>" + e.getMessage());
})
.and()
.csrf().disable()
//抛出的异常可以在此获得
.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.getWriter().write(accessDeniedException.getMessage());
}
});
return http.build();
}
}
step03-写一个过滤器,用户每申请一个请求就触碰这个类来解析JWT字符串
/**
* @author baize
* @date 2023/1/5
* @site www.qfedu.com
* <p>
* 这里专门用来解析 JWT 字符串,以后每个请求到达的时候都从这里去解析 JWT 字符串
*/
@Component
public class JwtFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
//当是登录接口的时候就不需要执行下面的解析,而是直接往下别的过滤器执行
if (req.getRequestURI().contains("/login")) {
//说明是一个登录请求
filterChain.doFilter(servletRequest, servletResponse);
return;
}
//获取请求头参数是Authorization来获取jwt字符串
String authorization = req.getHeader("Authorization");
//如果请求头中没有就在请求参数中获取
if (authorization == null || "".equalsIgnoreCase(authorization)) {
authorization = req.getParameter("token");
}
//如果jwt字符串不为空
if (authorization != null && !"".equals(authorization)) {
//因为jwt字符串的开头是以Bearer 存在的
authorization = authorization.replace("Bearer ", "");
Jws<Claims> claimsJws = null;
try {
//开始解析,将Key和jwt字符串放入
claimsJws = Jwts.parserBuilder().setSigningKey(SecurityConfig.KEY).build().parseClaimsJws(authorization);
} catch (Exception e) {
servletResponse.getWriter().write("jwt error");
return;
}
//获取配置文件中存储的丰富的数据类型的对象
Claims claims = claimsJws.getBody();
//获取存储在map中的权限
// ROLE_admin,ROLE_user
String authorities = (String) claims.get("authorities");
//将ROLE_admin,ROLE_user转为集合
List<SimpleGrantedAuthority> list = Arrays.stream(authorities.split(",")).map(a -> new SimpleGrantedAuthority(a)).collect(Collectors.toList());
//获取用户名
String username = claims.getSubject();
//将获取的用户名和权限存入到该类中
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, list);
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(servletRequest, servletResponse);
} else {
// throw new AuthenticationServiceException("jwt error");
servletResponse.getWriter().write("jwt error");
}
}
}
step04-在配置类中的抛出异常显示的权限不够可以用全局异常机制
@RestControllerAdvice
public class GlobalException {
@ExceptionHandler(AuthenticationServiceException.class)
public String authenticationException(AuthenticationServiceException e) {
return e.getMessage();
}
}
13.用户名密码分开提示
1.创造一个类,让该类来返回登录成功或者失败的信息
public class RespBean {
private Integer status;
private String msg;
private Object data;
public static RespBean ok(String msg, Object data) {
return new RespBean(200, msg, data);
}
public static RespBean ok(String msg) {
return new RespBean(200, msg, null);
}
public static RespBean error(String msg, Object data) {
return new RespBean(500, msg, data);
}
public static RespBean error(String msg) {
return new RespBean(500, msg, null);
}
private RespBean() {
}
private RespBean(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
2.配置登录时的配置类
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
//登陆成功时
.successHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.ok("登录成功", authentication.getPrincipal());
// 通过json将对象转成String类型传到前端
String s = new ObjectMapper().writeValueAsString(respBean);
out.write(s);
})
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = null;
if (e instanceof BadCredentialsException) {
respBean = RespBean.error("密码输入错误,登录失败");
} else if (e instanceof UsernameNotFoundException) {
respBean = RespBean.error("用户名输入错误,登录失败");
} else if (e instanceof AccountExpiredException) {
respBean = RespBean.error("账户过期,登录失败");
} else if (e instanceof LockedException) {
respBean = RespBean.error("账户被锁定,登录失败");
} else if (e instanceof CredentialsExpiredException) {
respBean = RespBean.error("密码过期,登录失败");
} else if (e instanceof DisabledException) {
respBean = RespBean.error("账户被禁用,登录失败");
} else {
respBean = RespBean.error("登录失败");
}
String s = new ObjectMapper().writeValueAsString(respBean);
out.write(s);
})
.and()
.csrf()
.disable()
.exceptionHandling()
//当访问一个需要认证的地址时,会调用到该方法
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("尚未登录,请登录");
String s = new ObjectMapper().writeValueAsString(respBean);
out.write(s);
})
//当权限不足的时候,抛出异常,会在这里被捕获
//即 throw new AccessDeniedException("权限不足,请联系管理员");抛出的异常,在这里处理
.accessDeniedHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.error(e.getMessage());
resp.getWriter().write(new ObjectMapper().writeValueAsString(respBean));
})
;
return http.build();
}
}
如果你想账户和密码分开提示,在.formLogin()上加这段代码
//对象后置处理器,Spring Security 默认已经帮我们处理好了 DaoAuthenticationProvider Bean,该 Bean 即将注册到 Spring 容器中,这里是一个对象的后置处理器,就是可以在这里对 Bean 的属性再做一次修改
.withObjectPostProcessor(new ObjectPostProcessor<UsernamePasswordAuthenticationFilter>() {
@Override
public <O extends UsernamePasswordAuthenticationFilter> O postProcess(O object) {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("{noop}123").roles("admin").build());
daoAuthenticationProvider.setUserDetailsService(manager);
object.setAuthenticationManager(new ProviderManager(daoAuthenticationProvider));
return object;
}
})
二.权限与角色
1.权限和角色的配置
step01-设置接口,每一种权限对应不同的接口访问
@RestController
public class HelloController {
/**
* 只要用户登录了就可以访问这个接口
*
* @return
*/
@GetMapping("/hello")
public String hello() {
return "hello";
}
/**
* 用户登录之后,必须具备 admin 这个角色才可以访问
* @return
*/
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
/**
* 用户登录之后,必须具备 user 角色才可以访问
* @return
*/
@GetMapping("/user/hello")
public String user() {
return "hello user";
}
}
step02-在UserService中,因为在登录的时候就要查看有什么权限了,所以在这里设置好权限返回给User实体类
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 根据用户名称查询用户对象,当用户登录的时候,这个方法会被自动触发
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
user.setRoles(userMapper.getUserRolesByUserId(user.getId()));
return user;
}
}
mapper接口
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
List<Role> getUserRolesByUserId(Integer uid);
}
<?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.qfedu.authorize.mapper.UserMapper">
<select resultType="com.qfedu.authorize.model.User" id="loadUserByUsername">
select * from user where username=#{username};
</select>
<select id="getUserRolesByUserId" resultType="com.qfedu.authorize.model.Role">
SELECT r.* FROM role r,user_role ur WHERE r.`id`=ur.`rid` AND ur.`uid`=#{uid}
</select>
</mapper>
step03-定义Role的实体类,User实体类中可能有多个Role还有配置权限的方法
public class Role {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private String address;
private Boolean enabled;
//一个用户可能有多个角色
private List<Role> roles;
/**
* 获取当前用户的权限/角色
*
* 当系统需要知道当前用户的角色/权限的时候,都是调用这个方法。这块跟 Shiro 不同,在 Spring Security 中,用户的权限和角色都是通过这个方法来返回的
*
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// for (Role role : roles) {
// SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getName());
// authorities.add(authority);
// }
return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
}
step04-配置角色
注意:角色记得要在数据库的前面加Role_
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
//访问 /admin/** 必须具备 admin 这个角色
//无论是 hasRole 还是 hasAuthority,最终调用的权限判断方法都是同一个,仅仅只是从参数上去区分到底是权限还是角色,如果参数是以 ROLE_ 开始的,则表示这是一个角色;否则就是一个权限字符串
.antMatchers("/admin/**").hasRole("admin")
//这里会自动给角色加上 ROLE_ 前缀
.antMatchers("/user/**").hasRole("user")
//剩下的所有请求,都必须登录之后才可以访问
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
return http.build();
}
}
2.通过注解来处理角色
1.角色权限的登录
step01-设置角色的注解,在Controller层,方法接口上写:@PreAuthorize("hasRole('user')"):方法执行之前进行校验有没有此角色,也可以用过@PreAuthorize("hasAuthortity('teacher:create')")权限
step02-还要在配置类中,开启上面的注解的权限:@EnableGlobalMethodSecurity(prePostEnabled = true)
step03-在service层,在登录验证的时候,顺便获取角色查询,但是在数据库中role字段的值必须有前缀ROLE_
step04-通过User方法里的方法setRoles,将从数据库中用户roles数据存入到内存中
step05-然后通过获取的roles放入到secutity里面,这是从数据库中获得的角色权限,这样就可以联想到@PreAuthorize("hasRole('user')")进行对比,这个用户是否有权限
2.角色的继承,层次关系(Hierarchy)
\r\n可以接着配多个层次关系
3.自定义权限匹配方法
1.spel(Spring express language)
@Test
void contextLoads() {
//定义的表达式
String exp1 = "1+2";
//表达式解析器
SpelExpressionParser parser = new SpelExpressionParser();
//解析字符串,将字符串的内容当成一个表达式执行
Expression expression = parser.parseExpression(exp1);
//获取执行的结果
Object value = expression.getValue();
System.out.println("value = " + value);
}
@Test
public void test01() {
User user = new User();
user.setUsername("张三");
//这个是计算的上下文对象
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("user", user);
//这个表达式表示获取 user 对象中的 username 属性值
// #user 表示获取 user 对象
String exp = "#user.username";
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(exp);
Object value = expression.getValue(ctx);
System.out.println("value = " + value);
}
@Test
public void test02() {
//设置上下文对象
StandardEvaluationContext ctx = new StandardEvaluationContext();
//获得User类
User user = new User();
ctx.setVariable("user", user);
//调用User对象的方法
String exp = "#user.saHello()";
//表达式分析器
SpelExpressionParser parser = new SpelExpressionParser();
//解析字符串
Expression expression = parser.parseExpression(exp);
Object value = expression.getValue(ctx);
System.out.println("value = " + value);
}