深入浅出Spring Security 王松
一、认证
- 你是谁?
- 用户信息由Authentication接口负责,认证工作由AuthenticationManager接口负责。
- 用户定义由UserDetails接口负责,数据源提供由UserDetailsService接口负责。
- 密码加密由PasswordEncoder接口负责
- 记住我由RememberMeService接口负责
一、登录成功
-
实际由AuthenticationSuccessHandler接口的三个实现类负责。
- SimpleUrlAuthenticationSuccessHandler
- SaveRequestAwareAuthenticationSuccessHandler,在SimpleUrlAuthenticationSuccessHandler基础上添加了请求缓存功能。
- ForwardAuthenticationSuccessHandler
- SimpleUrlAuthenticationSuccessHandler
-
由onAuthenticationSuccess方法完成。
一、defaultSuccessUrl
- 实际由SaveRequestAwareAuthenticationSuccessHandler类实现
- 也可由SimpleUrlAuthenticationFailureHandler类(相比较前者,少了缓存功能)实现
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index")
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll() // 跟登录相关的接口不做拦截
.and()
.csrf().disable()
.build();
}
}
- 用户在未认证的情况下访问页面,登录成功后自动从登陆页面重定向至该页面。
- 用户一开始访问的就是登陆页面,登陆成功后自动从登录页面重定向至其所指定的页面。
- 通过自身的重载方法,设置true既可变为请求转发方式跳转页面。
- 通过重定向方式跳转页面。
二、successForwardUrl
- 实际由ForwardAuthenticationSuccessHandler类实现
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.successForwardUrl("/index")
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll() // 跟登录相关的接口不做拦截
.and()
.csrf().disable()
.build();
}
}
- 无论在什么情况下,登录成功后自动请求转发至所指定的页面。。
- 通过请求转发方式跳转页面。
三、successHandler
- 重定向
- 方法一:生成新的SavedRequestAwareAuthenticationSuccessHandler对象,并设置需要的值。
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
// 需要在action中将doLogin改为doLogin?target=/hello
.successHandler(savedRequestAwareAuthenticationSuccessHandler())
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll() // 跟登录相关的接口不做拦截
.and()
.csrf().disable()
.build();
}
SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler() {
SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
handler.setDefaultTargetUrl("/index"); // 设置登录成功后重定向到达的页面
handler.setTargetUrlParameter("target"); // 设置传入参数的名称,并且登录成功后重定向至该页面
return handler;
}
}
-
请求转发
- 方法一:生成新的ForwardAuthenticationSuccessHandler对象,并设置需要的值。
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .loginProcessingUrl("/doLogin") .successHandler(forwardAuthenticationSuccessHandler()) .failureUrl("/login.html") .usernameParameter("uname") .passwordParameter("passwd") .permitAll() // 跟登录相关的接口不做拦截 .and() .csrf().disable() .build(); } ForwardAuthenticationSuccessHandler forwardAuthenticationSuccessHandler() { ForwardAuthenticationSuccessHandler handler = new ForwardAuthenticationSuccessHandler("/hello"); return handler; } }
-
前后端分离
-
方法一:创建类实现AuthenticationSuccessHandler接口,并且重写onAuthenticationSuccess方法。
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> resp = new HashMap<>();
resp.put("status", 200);
resp.put("msg", "登录成功!");
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(resp);
response.getWriter().write(s);
}
}
.successHandler(new MyAuthenticationSuccessHandler())
- 方法二:直接使用匿名内部类,重写onAuthenticationSuccess方法。
![AuthenticationFailureHandler](E:\Note\typora\images\AuthenticationFailureHandler.jpg).successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> resp = new HashMap<>();
resp.put("status", 200);
resp.put("msg", "登录成功!");
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(resp);
response.getWriter().write(s);
}
})
二、登录失败
- 实际由AuthenticationFailureHandler接口的五个实现类负责。
- SimpleUrlAuthenticationFailureHandler
- ExceptionMappingAuthenticationFailureHandler
- ForwardAuthenticationFailureHandler
- AuthenticationEntryPointFailureHandler
- DelegationAuthenticationFailureHandler
- SimpleUrlAuthenticationFailureHandler
- 由onAuthenticationFailure方法完成。
一、failureUrl
- 实际由SimpleUrlAuthenticationFailureHandler类实现
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index.html")
.failureUrl("/mylogin.html") // 登录失败后仍然重定向至登录页面,只能通过url携带错误信息
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable()
.build();
}
}
- 通过重定向方式跳转页面。
二、failureForwardUrl
- 实际由ForwardAuthenticationFailureHandler类实现
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index.html")
.failureForwardUrl("/mylogin.html") // 登录失败后请求转发至登陆页面,能够在服务器携带错误信息
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable()
.build();
}
}
- 通过请求转发方式跳转页面。
三、failureHandler
- 重定向
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index.html")
.failureHandler(simpleUrlAuthenticationFailureHandler())
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable()
.build();
}
SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler("/mylogin.html");
handler.setUseForward(false); // 默认就是false。
return handler;
}
}
- 请求转发
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index.html")
.failureHandler(simpleUrlAuthenticationFailureHandler())
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable()
.build();
}
SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler("/mylogin.html");
handler.setUseForward(true); // 为true时,请求转发方式跳转页面。
return handler;
}
}
-
前后端分离
- 方法一:创建类实现AuthenticationFailureHandler接口,并且重写onAuthenticationFailure方法。
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); Map<String, Object> resp = new HashMap<>(); resp.put("status", 500); resp.put("msg", "登录失败!" + exception.getMessage()); ObjectMapper om = new ObjectMapper(); String s = om.writeValueAsString(resp); response.getWriter().write(s); } }
.failureHandler(new MyAuthenticationFailureHandler())
- 方法二:直接使用匿名内部类,重写onAuthenticationFailure方法。
.failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); Map<String, Object> resp = new HashMap<>(); resp.put("status", 500); resp.put("msg", "登录失败!" + exception.getMessage()); ObjectMapper om = new ObjectMapper(); String s = om.writeValueAsString(resp); response.getWriter().write(s); } })
三、注销登录
一、logoutSuccessUrl
- 效果:只有一个注销地址和对应的结果。
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.mvcMatchers("/mylogin.html").permitAll() // 放行/mylogin.html接口用于注销后跳转
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout()
.logoutUrl("/logout") // 指定注销登录的请求地址,默认GET请求且地址为/logout
.invalidateHttpSession(true) // 是否使session失效,默认为true
.clearAuthentication(true) // 是否清楚认证信息,默认为true
.logoutSuccessUrl("/mylogin.html") // 注销后跳转的地址
.and()
.csrf().disable().build();
}
}
二、logoutRequestMatcher
- 效果:设置多个注销地址及对应的请求方法。
三、logoutSuccessHandler
-
前后端分离
- 效果:不同注销地址对应同一结果
@Configuration public class SecurityConfig { @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.authorizeRequests() // .mvcMatchers("/mylogin.html").permitAll() // 放行/mylogin.html接口用于注销后跳转 .anyRequest().authenticated() .and() .formLogin() .and() .logout() .logoutRequestMatcher(new OrRequestMatcher( new AntPathRequestMatcher("/logout1", "GET"), // 可以使用GET请求注销登录 new AntPathRequestMatcher("/logout2", "POST") // 可以使用POST请求注销登录 )) .invalidateHttpSession(true) // 是否使session失效,默认为true .clearAuthentication(true) // 是否清楚认证信息,默认为true .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); Map<String, Object> resp = new HashMap<>(); resp.put("status", 200); resp.put("msg", "注销成功!"); ObjectMapper om = new ObjectMapper(); String s = om.writeValueAsString(resp); response.getWriter().write(s); } }) .and() .csrf().disable().build(); } }
四、defaultLogoutSuccessHandlerFor
-
前后端分离
- 效果:不同注销地址对应不同的结果。
@Configuration public class SecurityConfig { @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .logout() .logoutRequestMatcher(new OrRequestMatcher( new AntPathRequestMatcher("/logout1", "GET"), // 可以使用GET请求注销登录 new AntPathRequestMatcher("/logout2", "POST") // 可以使用POST请求注销登录 )) .invalidateHttpSession(true) // 是否使session失效,默认为true .clearAuthentication(true) // 是否清楚认证信息,默认为true .defaultLogoutSuccessHandlerFor(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); Map<String, Object> resp = new HashMap<>(); resp.put("status", 200); resp.put("msg", "注销成功!111111"); ObjectMapper om = new ObjectMapper(); String s = om.writeValueAsString(resp); response.getWriter().write(s); } }, new AntPathRequestMatcher("/logout1", "GET")) .defaultLogoutSuccessHandlerFor(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); Map<String, Object> resp = new HashMap<>(); resp.put("status", 200); resp.put("msg", "注销成功!2222222"); ObjectMapper om = new ObjectMapper(); String s = om.writeValueAsString(resp); response.getWriter().write(s); } }, new AntPathRequestMatcher("/logout2", "POST")) .and() .csrf().disable().build(); } }
四、获取用户数据
一、SecurityContextHolder
一、单线程
@GetMapping("/user")
public String userInfo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.toString();
}
二、多线程
二、当前请求对象
@GetMapping("/authentication")
public String authentication(Authentication authentication) {
return authentication.toString();
}
@GetMapping("/principal")
public String principal(Principal principal) {
return principal.toString();
}
五、用户定义及数据源
一、InMemoryUserDetailsManager
- 自定义数据源
-
方法一:
-
@Autowired public void userDetailsService(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); UserDetails user = User.withUsername("root").roles("admin").password("{noop}12345").build(); manager.createUser(user); authenticationManagerBuilder.userDetailsService(manager); }
-
-
方法二:
-
@Bean public UserDetailsService userDetailsService() { // InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); UserDetails user1 = User.withUsername("root").roles("root").password("{noop}123").build(); UserDetails user2 = User.withUsername("admin").roles("admin").password("{noop}12345").build(); return new InMemoryUserDetailsManager(user1, user2); // manager.createUser(user1); // manager.createUser(user2); // return manager; }
-
二、JdbcUserDetailsManager
- 自定义数据源
-
方法一:
-
@Autowired public void Jdbc(AuthenticationManagerBuilder auth, DataSource dataSource) throws Exception { JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource); if (!manager.userExists("root")) { manager.createUser(User.withUsername("root").roles("root").password("{noop}12345").build()); } if (!manager.userExists("admin")) { manager.createUser(User.withUsername("admin").roles("admin").password("{noop}123456").build()); } auth.userDetailsService(manager); }
-
-
方法二:
-
@Bean public UserDetailsService Jdbc(DataSource dataSource) { JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource); if (!manager.userExists("root")) { manager.createUser(User.withUsername("root").roles("root").password("{noop}12345").build()); } if (!manager.userExists("admin")) { manager.createUser(User.withUsername("admin").roles("admin").password("{noop}123456").build()); } return manager; }
-
三、MyBatis
- 自定义用户
- 自定义数据源
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 DEFAULT CHARSET=utf8;
LOCK TABLES `role` WRITE;
/*!40000 ALTER TABLE `role` DISABLE KEYS */;
INSERT INTO `role` (`id`, `name`, `nameZh`)
VALUES
(1,'ROLE_dba','数据库管理员'),
(2,'ROLE_admin','系统管理员'),
(3,'ROLE_user','用户');
/*!40000 ALTER TABLE `role` ENABLE KEYS */;
UNLOCK TABLES;
# Dump of table 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,
`accountNonExpired` tinyint(1) DEFAULT NULL,
`accountNonLocked` tinyint(1) DEFAULT NULL,
`credentialsNonExpired` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;
INSERT INTO `user` (`id`, `username`, `password`, `enabled`, `accountNonExpired`, `accountNonLocked`, `credentialsNonExpired`)
VALUES
(1,'root','{noop}123',1,1,1,1),
(2,'admin','{noop}123',1,1,1,1),
(3,'sang','{noop}123',1,1,1,1);
/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;
# Dump of table 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`),
KEY `uid` (`uid`),
KEY `rid` (`rid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
LOCK TABLES `user_role` WRITE;
/*!40000 ALTER TABLE `user_role` DISABLE KEYS */;
INSERT INTO `user_role` (`id`, `uid`, `rid`)
VALUES
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);
@Configuration
public class SecurityConfig {
@Autowired
MyUserDetailsService myUserDetailsService;
// 以下代码可不用写,因为当自定义用户放在IOC容器中,默认会使用它
@Autowired
public void MyBatis(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
}
package com.miao.entity;
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 accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
@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 accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@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 getAccountNonExpired() {
return accountNonExpired;
}
public void setAccountNonExpired(Boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public Boolean getAccountNonLocked() {
return accountNonLocked;
}
public void setAccountNonLocked(Boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public Boolean getCredentialsNonExpired() {
return credentialsNonExpired;
}
public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", enabled=" + enabled +
", accountNonExpired=" + accountNonExpired +
", accountNonLocked=" + accountNonLocked +
", credentialsNonExpired=" + credentialsNonExpired +
", roles=" + roles +
'}';
}
}
package com.miao.entity;
public class Role {
private Integer id;
private String name;
private String nameZh;
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 String getNameZh() {
return nameZh;
}
public void setNameZh(String nameZh) {
this.nameZh = nameZh;
}
@Override
public String toString() {
return "Role{" +
"id=" + id +
", name='" + name + '\'' +
", nameZh='" + nameZh + '\'' +
'}';
}
}
package com.miao.entity;
import com.miao.mapper.UserMapper;
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;
import java.util.Objects;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("用户名不存在!");
}
user.setRoles(userMapper.getRolesByUid(user.getId()));
return user;
}
}
@Mapper
public interface UserMapper {
List<Role> getRolesByUid(Integer id);
User loadUserByUsername(String username);
}
<?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.miao.mapper.UserMapper">
<select id="loadUserByUsername" resultType="User">
SELECT * FROM user WHERE username=#{username}
</select>
<select id="getRolesByUid" resultType="Role">
SELECT r.* FROM role r, user_role ur WHERE r.id = ur.rid
</select>
</mapper>
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://101.43.117.227:3308/Security2?userUnicode=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
mybatis.type-aliases-package=com.miao.entity
mybatis.mapper-locations=classpath:**/*.xml
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
四、Spring Data JPA
六、密码加密
-
PasswordEncoder接口
- BCryptPasswordEncoder实现类
- ArgonnePasswordEncoder实现类
- Pbkdf2PassEncoder实现类
- SCryptPassEncoder实现类
- DelegatingPasswordEncoder工具类(由PasswordEncoderFactories类创建,且默认加密方式为BCryptPasswordEncoder)
-
全局的和局部的AuthenticationManager都是使用同一个DelegatingPasswordEncoder工具类或者BCryptPasswordEncoder实现类
一、加密
- 方案一:使用DelegatingPasswordEncode(默认使用BCryptPasswordEncoder),DelegatingPasswordEncode#encode,DelegatingPasswordEncode#matches+xxxEncoder#matches(默认是BCryptPasswordEncoder#matches)
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{bcrypt}$2a$10$5PxLt7t8yieNMGFG4dTxiOXjoWWed2MxdOevyW3bLXVoQh7.lUGNO").build());
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable().build();
}
}
- 方案二:使用BCryptPasswordEncoder,BCryptPasswordEncoder#encode,BCryptPasswordEncoder#matches
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("$2a$10$5PxLt7t8yieNMGFG4dTxiOXjoWWed2MxdOevyW3bLXVoQh7.lUGNO").build());
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable().build();
}
}
二、加密方案自动升级
- PasswordEncoder#upgradeEncoding、DaoAuthenticationProviderDaoAuthenticationProvider#createSuccessAuthentication
SELECT * FROM `user`CREATE DATABASE IF NOT EXISTS Security5 COLLATE utf8mb4_unicode_ci;
USE Security5;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `user` (`id`, `username`, `password`)
VALUES (1,'javaboy','{noop}123');
public class User implements UserDetails {
private Long id;
private String username;
private String password;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
Integer updatePassword(@Param("username") String username, @Param("newPassword") String newPassword);
}
<?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.miao.mapper.UserMapper">
<select id="loadUserByUsername" resultType="User">
SELECT * FROM user WHERE username = #{username}
</select>
<update id="updatePassword">
UPDATE user SET password = #{newPassword} WHERE username = #{username}
</update>
</mapper>
@Service
public class UserService implements UserDetailsService, UserDetailsPasswordService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userMapper.loadUserByUsername(username);
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
Integer result = userMapper.updatePassword(user.getUsername(), newPassword);
if (result == 1) {
((User) user).setPassword(newPassword); // 将最新的密码替换掉旧密码,用于后续认证
}
return user;
}
}
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable().build();
}
}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://101.43.117.227:3308/Security5?userUnicode=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
mybatis.type-aliases-package=com.miao.entity
mybatis.mapper-locations=classpath:**/*.xml
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
七、RememberMe
- key若不手动设置,则会生成UUID,每次都会更新,导致之前的remember-me失效。
一、RememberMe
- 重启服务器后不能自动登录
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}123").build());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.key("mykey") // 选择性使用
.and()
.csrf().disable().build();
}
}
二、持久化令牌
- 重启服务器后能自动登录
@Configuration
public class SecurityConfig {
@Autowired
private DataSource dataSource;
public JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource); // 该方法需要jdbc依赖
return jdbcTokenRepository;
}
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}123").build());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.key("mykey") // 必须使用,不然重启服务器后之前的remember-me用不了
.tokenRepository(jdbcTokenRepository()) // 指定JdbcTokenRepositoryImpl实例
.and()
.csrf().disable().build();
}
}
三、二次检验
- 重启服务器后能自动登录
@Configuration
public class SecurityConfig {
@Autowired
private DataSource dataSource;
public JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("user").roles("root").password("{noop}123").build());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/admin").fullyAuthenticated()
.antMatchers("/rememberme").rememberMe()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.key("mykey") // 必须使用,不然重启服务器后之前的remember-me用不了
.tokenRepository(jdbcTokenRepository())
.and()
.csrf().disable().build();
}
}
@RestController
public class MyController {
@GetMapping("/hello") // 认证后可以访问,无论何种认证方式
public String hello() {
return "hello";
}
@GetMapping("/admin") // 认证后可以访问,必须是用户名/密码方式
public String admin() {
return "admin";
}
@GetMapping("/rememberme") // 必须先rememberme,然后重启浏览器才能访问
public String rememberme() {
return "rememberme";
}
}
八、会话管理
一 、会话并发
- 重定向
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}123").build());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/hi").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.sessionManagement() // 开启会话并发
.maximumSessions(1) // 最大并发数
.expiredUrl("/hi") // 并挤下线后重定向的地址
.and()
.and().build();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher(); // 用于维护当前HttpSession记录
}
}
- 禁止后来者登录
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}123").build());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/hi").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.sessionManagement() // 开启会话并发
.maximumSessions(1) // 最大并发数
.maxSessionsPreventsLogin(true) // 禁止后来者登录
.and()
.and().build();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher(); // 用于维护当前HttpSession记录
}
}
- 前后端分离
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}123").build());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/hi").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.sessionManagement() // 开启会话并发
.maximumSessions(1) // 最大并发数
.expiredSessionStrategy(new SessionInformationExpiredStrategy() {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> resp = new HashMap<>();
resp.put("status", 500);
resp.put("msg", "当前会话已经失效,请重新登录");
String s = new ObjectMapper().writeValueAsString(resp);
response.getWriter().write(s);
}
})
.and()
.and().build();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher(); // 用于维护当前HttpSession记录
}
}
二、会话固定攻击与防御
三、Session共享
- 集群环境下使用
九、HttpFirewall
一、严格模式
二、普通模式
十、漏洞保护
一、CSRF攻击与防御
二、HTTP响应头处理
一、缓存控制
二、X-Content-Type-Options
三、Strict-Transport-Security
四、X-Frame-Options
五、X-XSS-Protection
六、Content-Security-Policy
七、Referrer-Policy
八、Feature-policy
九、Clear-Site-Data
三、HTTP通信安全
一、HTTPS
1. 非正常使用
- 生成证书
keytool -genkey -alias tomcathttps -keyalg RSA -keysize 2048 -keystore myhttps.p12 -validity 365
口令:111111
是否正确:y
- 配置证书
server.ssl.key-store=classpath:myhttps.p12
server.ssl.key-alias=tomcathttps
server.ssl.key-store-password=111111
- 访问
https://localhost:8080/login
输入:thisisunsafe
2. 正常使用
- http默认端口是8080,https默认端口为8443。(即访问http://localhost:8080/https会转发到https://localhost:8443/https)
- 项目端口与https默认端口一致(8443)(即访问http://localhost:8080/https会转发到https://localhost:8443/https)
server.ssl.key-store=classpath:myhttps.p12
server.ssl.key-alias=tomcathttps
server.ssl.key-store-password=111111
server.port=8443
@Configuration
public class TomcatConfig {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addAdditionalTomcatConnectors(createTomcatConnector());
return factory;
}
private Connector createTomcatConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8080); // 监听8080端口
return connector;
}
}
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.requiresChannel() // 开启配置
.antMatchers("/https").requiresSecure() // 该接口的请求是https协议,若不是则使用https协议重定向https://localhost:8443/https
.antMatchers("/http").requiresInsecure() // 该接口的请求是http协议,若不是则使用http协议重定向http://localhost:8080/http
.and()
.csrf().disable().build();
}
}
- 项目端口与https默认端口不一致(8443)(即访问http://localhost:8080/https会转发到https://localhost:8443/https,而不是https://localhost:8444/https)
server.ssl.key-store=classpath:myhttps.p12
server.ssl.key-alias=tomcathttps
server.ssl.key-store-password=111111
server.port=8444
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.portMapper() // 开启配置
.http(8080).mapsTo(8444) // 设置8080端口转发至8444端口
.and()
.requiresChannel()
.antMatchers("/https").requiresSecure()
.antMatchers("/http").requiresInsecure()
.and()
.csrf().disable().build();
}
}
二、代理服务器配置
十一、HTTP认证
一、HTTP Basic authentication
- HTTP基本认证,不安全
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic() // 不再formLogin()
.and()
.csrf().disable().build();
}
}
二、HTTP Digest authentication
- HTTP摘要认证,安全,但不支持BCrypt、PBKDF2、SCrypt加密方式
十二、跨域问题
一、Spring处理方案
一、@CrossOrigin
- DisptacherServlet中触发,可与addCorsMappings合并使用
@RestController
public class MyController {
/*
* allowCredentials:浏览器是否应当发送凭证信息,如Cookie。
* allowedHeaders:请求被允许的请求头字段,*表示所有字段。
* expossedHeaders:哪些响应头可以作为响应的一部分暴露出来。注意,这里可以一一列举,通配符*无效。
* maxAge:预检请求的有效期,有效期内不必再次发生预检请求,默认是1800秒。
* methods:允许的请求方法,*表示允许所有方法。
* origins:允许的域,*表示允许所有的域。
* */
@CrossOrigin(origins = "http://localhost:8081")
@PostMapping("/post")
public String post() {
return "hello post";
}
}
二、addCorsMappings
-
DisptacherServlet中触发,可与@CrossOrigin合并使用
-
重写WebMvcConfigurer#addCorsMappings
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("*")
.allowedOrigins("*")
.allowedHeaders("*")
.allowCredentials(false)
.exposedHeaders("")
.maxAge(3600);
}
}
三、CorsFilter
- 过滤器中触发,早于前两种方式触发,与前两者一起用会降低性能
@Configuration
public class WebMvcConfigFilter {
@Bean
FilterRegistrationBean<CorsFilter> corsFilterFilterRegistrationBean() {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
registrationBean.setFilter(new CorsFilter(source));
registrationBean.setOrder(-1);
return registrationBean;
}
}
二、Spring Security处理方案
一、特殊处理OPTIONS请求
-
确保在Spring Security中能正常使用Spring中前两种方法
-
不安全且不优雅,了解即可
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll() // 放行OPTIONS请求,使@CorsOrigin和重写addCorsMappings方法有效
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable().build();
}
}
二、CorsFilter
- 只需要将该过滤器优先级高于Spring Security过滤器优先级即可
@Configuration
public class WebMvcConfigFilter {
@Bean
FilterRegistrationBean<CorsFilter> corsFilterFilterRegistrationBean() {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
registrationBean.setFilter(new CorsFilter(source));
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 设置为最高优先级
return registrationBean;
}
}
三、*专业处理方案
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.cors()
.configurationSource(configurationSource())
.and()
.csrf().disable().build();
}
CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
十三、异常处理
- 主要由过滤器ExceptionTranslationFilter处理
一、认证异常
- AuthenticationException
二、权限异常
- AccessDeniedException
三、自定义异常
一、不使用默认处理器
- 所有接口共用同一个处理方案
@Configuration
public class SeccurityConfig {
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("user").password("{noop}123").build());
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/admin").hasRole("admin") // 需要有admin角色才能访问admin接口
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("uname")
.passwordParameter("passwd")
.defaultSuccessUrl("/admin")
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("please login");
}
}) // 认证异常
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("forbidden");
}
}) // 权限异常
.and()
.csrf().disable().build();
}
}
二、使用默认处理器
- 不同接口可对应不同处理方案
@Configuration
public class SeccurityConfig {
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("wx").roles("wxadmin").password("{noop}123").build(),
User.withUsername("qq").roles("user").password("{noop}123").build()
);
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
AntPathRequestMatcher matcher1 = new AntPathRequestMatcher("/wx/admin");
AntPathRequestMatcher matcher2 = new AntPathRequestMatcher("/qq/admin");
return http.authorizeRequests()
.antMatchers("/wx/admin").hasRole("wxadmin")
.antMatchers("/qq/admin").hasRole("qqadmin")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("uname")
.passwordParameter("passwd")
.defaultSuccessUrl("/index")
.permitAll()
.and()
.exceptionHandling()
.defaultAuthenticationEntryPointFor(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("text/plain;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("请登录,微信用户");
}
}, matcher1)
.defaultAuthenticationEntryPointFor(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("text/plain;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("请登录,QQ用户");
}
}, matcher2)
.defaultAccessDeniedHandlerFor(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("text/plain;charset=UTF-8");
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("权限不足,微信用户");
}
}, matcher1)
.defaultAccessDeniedHandlerFor(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("text/plain;charset=UTF-8");
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("权限不足,QQ用户");
}
}, matcher2)
.and()
.csrf().disable().build();
}
}
二、授权
- 你可以做什么?
一、权限管理
一、核心
一、前置处理器
二、后置处理器
三、权限元数据
四、权限表达式
二 、基于URL地址(过滤器)的权限管理
一、基本使用
@RestController
public class MyController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
@GetMapping("/user/hello")
public String user() {
return "hello user";
}
@GetMapping("/getinfo")
public String getInfo() {
return "getinfo";
}
}
@Configuration
public class SecurityConfig {
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("admin").roles("ADMIN").password("{noop}123").build(),
User.withUsername("root").roles("USER").password("{noop}123").build(),
User.withUsername("reader").authorities("READ_INFO").password("{noop}123").build());
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") // 必须具有ADMIN角色才可以访问,会自动加上ROLE_前缀
.antMatchers("/user/**").access("hasAnyRole('USER', 'ADMIN')") // 使用access来使用权限表达式,拥有USER、ADMIN任意角色都可以访问,会自动加上ROLE_前缀
.antMatchers("/getinfo").hasAuthority("READ_INFO") // 必须具有READ_INFO权限才可以访问,不会加上ROLE_前缀
.anyRequest().access("isAuthenticated()") // 是要是认证过的用户都可以访问
.and()
.formLogin()
.and()
.csrf().disable().build();
}
}
二、角色继承
@Configuration
public class SecurityConfig {
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER"); // 角色ADMIN继承了角色USER
return hierarchy;
}
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("admin").roles("ADMIN").password("{noop}123").build(),
User.withUsername("root").roles("USER").password("{noop}123").build(),
User.withUsername("reader").authorities("READ_INFO").password("{noop}123").build());
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") // 必须具有ADMIN角色才可以访问,会自动加上ROLE_前缀
.antMatchers("/user/**").access("hasAnyRole('USER')") // 使用access来使用权限表达式,拥有USER角色可以访问,ADMIN也可以访问,因为继承了USER,会自动加上ROLE_前缀
.antMatchers("/getinfo").hasAuthority("READ_INFO") // 必须具有READ_INFO权限才可以访问,不会加上ROLE_前缀
.anyRequest().access("isAuthenticated()") // 是要是认证过的用户都可以访问
.and()
.formLogin()
.and()
.csrf().disable().build();
}
}
三、自定义表达式
- 注意第一个字母要变成小写的!!!如MyExpression类:@myExpression
@RestController
public class MyController {
@GetMapping("/hello/{userId}")
public String hello(@PathVariable Integer userId) {
return "hello" + userId;
}
@GetMapping("/hi")
public String hello2User(String username) {
return "hello" + username;
}
}
@Component
public class MyExpression {
public boolean checkId(Authentication authentication, Integer userId) {
// 如果通过认证,才进行授权鉴定
if (authentication.isAuthenticated()) {
// 要求id必须为偶数
return userId % 2 == 0;
} else {
return false;
}
}
public boolean check(HttpServletRequest request) {
// 要求请求参数username必须为root
// localhost:8080/hi?username=root
return "root".equals(request.getParameter("username"));
}
}
@Configuration
public class SecurityConfig {
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("USER").password("{noop}123").build());
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/hello/{userId}")
.access("@myExpression.checkId(authentication, #userId)")
.antMatchers("/hi")
.access("isAuthenticated() and @myExpression.check(request)")
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable().build();
}
}
四、动态权限管理
# ************************************************************
# Sequel Pro SQL dump
# Version 4541
#
# http://www.sequelpro.com/
# https://github.com/sequelpro/sequelpro
#
# Host: 127.0.0.1 (MySQL 5.7.26)
# Database: security13
# Generation Time: 2020-09-22 08:10:28 +0000
# ************************************************************
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
# Dump of table menu
# ------------------------------------------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pattern` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
LOCK TABLES `menu` WRITE;
/*!40000 ALTER TABLE `menu` DISABLE KEYS */;
INSERT INTO `menu` (`id`, `pattern`)
VALUES
(1,'/admin/**'),
(2,'/user/**'),
(3,'/guest/**');
/*!40000 ALTER TABLE `menu` ENABLE KEYS */;
UNLOCK TABLES;
# Dump of table menu_role
# ------------------------------------------------------------
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `mid` (`mid`),
KEY `rid` (`rid`),
CONSTRAINT `menu_role_ibfk_1` FOREIGN KEY (`mid`) REFERENCES `menu` (`id`),
CONSTRAINT `menu_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
LOCK TABLES `menu_role` WRITE;
/*!40000 ALTER TABLE `menu_role` DISABLE KEYS */;
INSERT INTO `menu_role` (`id`, `mid`, `rid`)
VALUES
(1,1,1),
(2,2,2),
(3,3,3),
(4,3,2);
/*!40000 ALTER TABLE `menu_role` ENABLE KEYS */;
UNLOCK TABLES;
# Dump of table 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 DEFAULT CHARSET=utf8;
LOCK TABLES `role` WRITE;
/*!40000 ALTER TABLE `role` DISABLE KEYS */;
INSERT INTO `role` (`id`, `name`, `nameZh`)
VALUES
(1,'ROLE_ADMIN','系统管理员'),
(2,'ROLE_USER','普通用户'),
(3,'ROLE_GUEST','游客');
/*!40000 ALTER TABLE `role` ENABLE KEYS */;
UNLOCK TABLES;
# Dump of table 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 DEFAULT CHARSET=utf8;
LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;
INSERT INTO `user` (`id`, `username`, `password`, `enabled`, `locked`)
VALUES
(1,'admin','{noop}123',1,0),
(2,'user','{noop}123',1,0),
(3,'javaboy','{noop}123',1,0);
/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;
# Dump of table 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`),
KEY `uid` (`uid`),
KEY `rid` (`rid`),
CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`),
CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
LOCK TABLES `user_role` WRITE;
/*!40000 ALTER TABLE `user_role` DISABLE KEYS */;
INSERT INTO `user_role` (`id`, `uid`, `rid`)
VALUES
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);
/*!40000 ALTER TABLE `user_role` ENABLE KEYS */;
UNLOCK TABLES;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
1.非所有URL必须配置数据库
2.所有URL必须配置数据库
三、基于方法(AOP)的权限管理
二、权限模型
一、ACL
二、RBAC
三、OAuth2
- 第三方登录
一、授权
一、四种授权模式
二、第三方登录
一、github
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login() // 开启OAuth2登录
.and()
.csrf().disable().build();
}
}
@RestController
public class MyController {
@GetMapping("/hello")
public DefaultOAuth2User github() {
return ((DefaultOAuth2User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
}
@GetMapping("/demo1")
public String demo1() {
return "demo1";
}
}
spring.security.oauth2.client.registration.github.client-id=3eb7e298221d49d1780c
spring.security.oauth2.client.registration.github.client-secret=356e413e7b28bd62bc64327c0ce79bb11504711e
二、gitee
三、授权服务器与资源服务器
二、Redis
三、JWT
四、认证流程分析
一、认证五类(自下而上)
一、AuthenticationManager接口类
- ProviderManager实现类(默认)。
- 自定义实现类。
二、ProviderManager实现类
-
可以管理多个AuthenticationProvider接口类(列表)
-
private List<AuthenticationProvider> providers
-
-
优先使用本类中的AuthenticationProvider提供的认证方法,都认证失败,则调用父类的认证方法(AuthenticationManager或其子类(ProviderManager实现类))
三、*AuthenticationProvider接口类
-
核心认证类,提供认证方法,可以放入ProviderManager中管理。
-
用户名/密码认证:DaoAuthenticationProvider实现类(继承AbstractUserDetailsAuthenticationProvider抽象类,并且具体认证逻辑也在这个抽象类中)。
- 核心认证方法是authenticate,在其中调用其他方法的顺序:preAuthenticationChecks---->additionalAuthenticationChecks—>postAuthenticationChecks—>createSuccessAuthentication(创建UsernamePasswordAuthenticationToken对象)
-
记住我认证:RememberMeAuthenticationProvider实现类。
四、Authentication接口类
- 用户名/密码认证:UsernamePasswordAuthenticationToken实现类
五、AbstractAuthenticationProcessingFilter抽象类
- 用户名/密码认证:UsernamePasswordAuthenticationFilter实现类
- 由于Authentication对应的实例是UsernamePasswordAuthenticationtoken类型,AuthenticationManager对应的实例是ProviderManager类型,故流程图:UsernamePasswordAuthenticationFilter(登录参数在此提取)—>UsernamePasswordAuthenticationtoken—>ProviderManager
二、配置多个数据源
- 每个AuthenticationProvider中都有一个UserDetailsService。
- 在AuthenticationProvider(DaoAuthenticationProvider实现类)中设置两个数据源,再放入ProviderManager的集合中。
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager() {
UserDetails user1 = User.withUsername("root").roles("root").password("{noop}1234").build();
UserDetails user2 = User.withUsername("admin").roles("admin").password("{noop}12345").build();
InMemoryUserDetailsManager u1 = new InMemoryUserDetailsManager(user1);
InMemoryUserDetailsManager u2 = new InMemoryUserDetailsManager(user2);
DaoAuthenticationProvider dao1 = new DaoAuthenticationProvider();
DaoAuthenticationProvider dao2 = new DaoAuthenticationProvider();
dao1.setUserDetailsService(u1);
dao2.setUserDetailsService(u2);
return new ProviderManager(dao1, dao2);
}
}
三、添加验证码
public class KaptchaAuthenticationProvider extends DaoAuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
HttpServletRequest req = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); // 获取当前请求对象
String kaptcha = req.getParameter("kaptcha"); // 获取页面提交的验证码文本
String sessionKaptcha = (String) req.getSession().getAttribute("kaptcha"); // 获取存储在session中的验证码文本(在验证码图像生成前就放入了session)
if (Objects.nonNull(kaptcha) && Objects.nonNull(sessionKaptcha) && kaptcha.equalsIgnoreCase(sessionKaptcha)) {
return super.authenticate(authentication);
}
throw new AuthenticationServiceException("验证码输入错误");
}
}
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager() {
KaptchaAuthenticationProvider provider = new KaptchaAuthenticationProvider();
provider.setUserDetailsService(new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}12345").build()));
return new ProviderManager(provider);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/vc.jpg").permitAll() // 对验证码图片接口放行
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index")
.failureForwardUrl("/mylogin.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable().build();
}
}
@Controller
public class MyController {
@Autowired
private Producer producer;
@GetMapping("/vc.jpg")
public void getVerifyCode(HttpServletResponse response, HttpSession session) {
response.setContentType("image/jpeg");
String text = producer.createText(); // 生成验证码文本,并存入session中用来后面的验证
session.setAttribute("kaptcha", text);
BufferedImage image = producer.createImage(text); // 根据验证码文本生成验证码图片
try {
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@RequestMapping("/mylogin.html")
public String mylogin() {
return "mylogin";
}
@ResponseBody
@RequestMapping("/index")
public String index() {
return "index";
}
}
四、注意点
- 默认有局部的AuthenticaionManager(有AnonymousProvider)
- 默认有全局的AuthenticationManager(有DaoAuthenticationProvider,即生成初始user及其密码)
一、全局设置AuthenticationMananger、AuthenticationProvider、UserDetailsService
- @Bean返回AuthenticationManager,则是GlobalAuthenticationManager
- 即没有user,GlobalAuthenticationManager被代替了
@Bean
ProviderManager provider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new InMemoryUserDetailsManager(User.withUsername("root").roles(("root")).password("{noop}123").build()));
ProviderManager manager = new ProviderManager(provider);
return manager;
}
- @Bean返回AuthenticationProvider,则是为GlobalAuthenticationManager配备AuthenticationProvider。
- 即没有user,userDetailsService被自定义的代替了
@Bean
AuthenticationProvider provider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new InMemoryUserDetailsManager(User.withUsername("root").roles(("root")).password("{noop}123").build()));
return provider;
}
- @Bean返回userDetailsService,改变了GlobalAuthenticationManager中的userDetailsService
- 即没有user,userDetailsService被自定义的代替了
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles(("root")).password("{noop}123").build());
}
二、局部设置AuthenticationMananger、AuthenticationProvider、UserDetailsService
- 局部设置AuthenticationMananger优先级高于@Bean,GlobalAuthenticationManager仍然存在但是不再生效
- 即仍然有user,但是无法进行使用
.authenticationManager(manager());
- 局部设置AuthenticationProvider配备给局部LocalAuthenticationProvider(则此时不仅有AnonymousProvider,还有DaoAuthenticationProvider)
- 即仍然有user,且能够正常使用
.authenticationProvider(provider())
- 局部设置userDetailsService,会自动生成一个AuthenticationProvider配备给LocalAuthenticationManager(则此时不仅有AnonymousProvider,还有DaoAuthenticationProvider)
- 即仍然有user,且能够正常使用
.userDetailsService(userDetailsService())
五、过滤器链流程分析
优先级排序:
ForceEagerSessionCreationFilter
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter
一、JSON登录
package com.miao.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class LoginFilter 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("application/json;charset=UTF-8")) {
Map<String, String> userInfo = new HashMap<>();
try {
userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
e.printStackTrace();
}
}
return super.attemptAuthentication(request, response);
}
}
package com.miao.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.miao.filter.LoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.parameters.P;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class SecurityConfig {
// 全局AuthenticationManager
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public UserDetailsService GlobaluserDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}12345").build());
}
// 局部AuthenticationManager
public AuthenticationManager LocalauthenticationManager() {
DaoAuthenticationProvider dao = new DaoAuthenticationProvider();
dao.setUserDetailsService(LoacluserDetailsService());
ProviderManager manager = new ProviderManager(dao);
return manager;
}
public UserDetailsService LoacluserDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("admin").roles("admin").password("{noop}12345").build());
}
public LoginFilter loginFilter() throws Exception {
LoginFilter filter = new LoginFilter();
// 全局AuthenticationManager
// filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
// 局部AuthenticationManager
AuthenticationManager authenticationManager = LocalauthenticationManager();
filter.setAuthenticationManager(authenticationManager);
filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> resp = new HashMap<>();
resp.put("status", 200);
resp.put("msg", "登录成功" + authentication);
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(resp);
response.getWriter().write(s);
}
});
return filter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable().build();
}
}
二、添加验证码
@Configuration
public class SecurityConfig {
@Autowired
public AuthenticationConfiguration authenticationConfiguration;
public LoginFilter loginFilter() throws Exception {
LoginFilter filter = new LoginFilter();
filter.setFilterProcessesUrl("/doLogin"); // 设置该过滤器生效地址,默认是/login
filter.setUsernameParameter("uname"); // 必须在此处设置,在http中设置不会生效
filter.setPasswordParameter("passwd"); // 必须在次数设置,在http中设置不会生效
filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/index"));
filter.setAuthenticationFailureHandler(new ForwardAuthenticationFailureHandler("/mylogin.html")); // 将会显示错误信息
return filter;
}
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withUsername("root").roles("root").password("{noop}12345").build());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/vc.jpg").permitAll() // 对验证码图片接口放行
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/doLogin")
// .usernameParameter("uname") // 有自定义过滤器的情况下不生效
// .passwordParameter("passwd") // 有自定义过滤器的情况下不生效
.permitAll()
.and()
.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable().build();
}
}
public class LoginFilter 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());
}
String kaptcha = request.getParameter("kaptcha");
String sessionKaptcha = (String) request.getSession().getAttribute("kaptcha");
if (Objects.nonNull(kaptcha) && Objects.nonNull(sessionKaptcha) && kaptcha.equalsIgnoreCase(sessionKaptcha)) {
return super.attemptAuthentication(request, response); // 仍然使用表单登录方式认证
}
throw new AuthenticationServiceException("验证码输入错误");
}
}