引入相关依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
准备好数据库表, 以及初始化数据
CREATE TABLE IF NOT EXISTS mooc_roles (
id BIGINT NOT NULL AUTO_INCREMENT,
role_name VARCHAR(50) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_mooc_roles_role_name UNIQUE (role_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS mooc_users (
id BIGINT NOT NULL AUTO_INCREMENT,
account_non_expired BIT NOT NULL,
account_non_locked BIT NOT NULL,
credentials_non_expired BIT NOT NULL,
email VARCHAR(254) NOT NULL,
enabled BIT NOT NULL,
mobile VARCHAR(11) NOT NULL,
name VARCHAR(50) NOT NULL,
password_hash VARCHAR(80) NOT NULL,
username VARCHAR(50) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_mooc_users_username UNIQUE (username),
CONSTRAINT uk_mooc_users_mobile UNIQUE (mobile),
CONSTRAINT uk_mooc_users_email UNIQUE (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS mooc_users_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id),
CONSTRAINT fk_users_roles_user_id_mooc_users_id FOREIGN KEY (user_id) REFERENCES mooc_users (id),
CONSTRAINT fk_users_roles_role_id_mooc_roles_id FOREIGN KEY (role_id) REFERENCES mooc_roles (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
insert into mooc_users(id, username, `name`, mobile, password_hash, enabled, account_non_expired, account_non_locked, credentials_non_expired, email)
values (1, 'user', 'zhnagsan', '13000000001', '{bcrypt}$2a$10$BFtL.Gx1qoIX1YmdCM0jwe8/DWCZ8yCX2ut.bc3y3I4.ab8ork1NC', 1, 1, 1, 1, 'zhangsan@local.dev'),
(2, 'old_user', 'lisi', '13000000002', '{SHA-1}85a4f70357bd47d7a76cac02b7584045482fd9b0', 1, 1, 1, 1, 'lisi@local.dev');
insert into mooc_roles(id, role_name) values (1, 'ROLE_USER'), (2, 'ROLE_ADMIN');
insert into mooc_users_roles(user_id, role_id) values (1, 1), (1, 2), (2, 1);
添加yml配置
#spring.security.user.name=user
#spring.security.user.password=12345
#spring.security.user.roles=ADMIN,USER
logging:
level:
com:
example.example.uaa: DEBUG
org:
springframework:
security: DEBUG
jdbc: TRACE
spring:
# H2数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接url
# MODE=MySQL:兼容MySQL写法
# DATABASE_TO_LOWER=TRUE :表名转小写
# CASE_INSENSITIVE_IDENTIFIERS=TRUE : 不区分大小写
# DB_CLOSE_DELAY=-1 : 不自动关闭数据库连接
url: jdbc:mysql://192.168.0.101:3306/uaa?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
username: aesop
password: aesop
jpa:
hibernate:
# 官方说明: https://docs.spring.io/spring-boot/docs/2.7.12/reference/html/howto.html#howto.data-initialization.using-jpa
ddl-auto: none
database-platform: org.hibernate.dialect.MySQL8Dialect
database: MySQL
# 官方说明:https://docs.spring.io/spring-boot/docs/2.7.12/reference/html/howto.html#howto.data-initialization
# 这将推迟数据源初始化,直到创建和初始化任何 EntityManagerFactory beans 之后
# 解决了data.sql文件执行报错的问题,原理就是创建好表实体产生表结构后,再执行初始化
defer-datasource-initialization: true
SecurityConfig 核心配置类
package com.example.uaa.config;
import com.example.uaa.dao.UserRepo;
import com.example.uaa.filter.RestAuthenticationFilter;
import com.example.uaa.service.impl.UserDetailServiceImpl;
import com.example.uaa.service.impl.UserDetailsPasswordServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.passay.MessageResolver;
import org.passay.spring.SpringMessageResolver;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import javax.sql.DataSource;
import java.util.Map;
/**
* core配置
*/
@Slf4j
@Configuration
@EnableWebSecurity(debug = false) // 开启调试模式,生产环境不要使用
@RequiredArgsConstructor
public class SecurityConfig {
private final ObjectMapper objectMapper;
private final AuthenticationConfiguration authenticationConfiguration;
private final MessageSource messageSource;
// private final DataSource dataSource;
private final UserRepo userRepo;
// @Bean
// public DataSource dataSource() {
// return new EmbeddedDatabaseBuilder()
// .setType(EmbeddedDatabaseType.H2)
// .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)// 自动建表
// .build();
// }
/**
* 注册passay的国际化消息处理类
*
* @return
*/
@Bean
public MessageResolver messageResolver() {
return new SpringMessageResolver(messageSource);
}
/**
* 自定义验证注解,添加国际化消息处理类
* @return
*/
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((req) -> req
// 需要认证并拥有特定角色才能访问的URL
.antMatchers("/api/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
// 放行不需要认证的URL
.antMatchers("/authorize/**").permitAll()
.antMatchers("/webjars/**", "/error", "/h2-console/**").permitAll()
// 放行swagger3
.antMatchers("/swagger-ui/**", "/swagger-resources/**", "/v3/**", "/doc.html").permitAll()
// 放行通用的css、js等静态资源位置
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
// 除了以上规则,默认拦截所有URL请求,要求认证后才能访问(该语句必须要放在末尾)
.anyRequest().authenticated()
)
// 用于配置H2控制台界面的正常显示 https://docs.spring.io/spring-boot/docs/2.7.11/reference/html/data.html#data.sql.h2-web-console.spring-security
.headers((headers) -> headers.frameOptions().sameOrigin())
// 添加自定义登录过滤器,替换掉默认的UsernamePasswordAuthenticationFilter过滤器
.addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 以下表单登录,被restAuthenticationFilter配置替换:
// .formLogin(form-> form
// // 定义登陆页面
// .loginPage("/login").permitAll()
// // 登录成功后跳转的页面
// // .defaultSuccessUrl("/")
// // 登录成功后置处理器。会覆盖掉defaultSuccessUrl的配置
// .successHandler(((request, response, authentication) -> {
// ObjectMapper objectMapper = new ObjectMapper();
// response.setStatus(HttpStatus.OK.value());
// response.getWriter().println("LoginSuccess:"+
// objectMapper.writeValueAsString(authentication));
// }))
// // 登录失败后置处理器,不配置,默认跳转/login?error
// .failureHandler(((request, response, exception) -> {
// response.setStatus(HttpStatus.UNAUTHORIZED.value());
// response.getWriter().println("LoginFailure!");
// }))
// )
// 配置退出登录的路径,配置后使用POST请求。若不配置,默认为/logout,退出成功跳到/login
// .logout(logout-> logout.logoutUrl("/logout").logoutSuccessUrl("/login"))
.httpBasic(Customizer.withDefaults())
// 开启CSRF ,并配置不需要CSRF的路径
// .csrf(csrf->csrf.ignoringAntMatchers("/api/**"))
// .csrf()
// 禁用csrf
.csrf().disable()
.rememberMe(Customizer.withDefaults());
// ...
return http.build();
}
/**
* 设定认证方式 Authentication
* 代替了配置
* spring.security.user.name=user
* spring.security.user.password=12345
* spring.security.user.roles=ADMIN,USER
*
* 基于spring boot 2.7 后新的认证方式:
* https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
*/
/**
* 基于内存的认证 InMemoryUserDetailsManager
*/
// @Bean
// public InMemoryUserDetailsManager userDetailsService() {
// UserDetails user = User.builder()
// .username("user")
// .password(passwordEncoder().encode("123456"))
// .roles("USER")
// .build();
// return new InMemoryUserDetailsManager(user);
// }
/**
* 基于JDBC的连接H2
* @return
*/
// @Bean
// public UserDetailsManager userDetailsManager() {
// UserDetails user = User.builder()
// .username("user")
// .password(passwordEncoder().encode("123456"))
// .roles("USER")
// .build();
// JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource());
// users.createUser(user);
// return users;
// }
@Bean
public UserDetailsService userDetails() {
return new UserDetailServiceImpl(userRepo);
}
@Bean
public UserDetailsPasswordService userDetailsPasswordService() {
return new UserDetailsPasswordServiceImpl(userRepo);
}
/**
* 使用BCrypt密码加密类型,这里还可以增加其他类型,如果增加其他类型,表示对其他的加密类型进行兼容处理,而新的加密则默认使用的BCryptPasswordEncoder加密算法。
*
* 简单地理解为,遇到新密码,DelegatingPasswordEncoder会委托给BCryptPasswordEncoder(encodingId为bcryp*)进行加密,同时,
* 对历史上使用ldap,MD4,MD5等等加密算法的密码认证保持兼容(如果数据库里的密码使用的是MD5算法,那使用matches方法认证仍可以通过,但新密码会使bcrypt进行储存),
*
* 支持的一些加密类型{@link PasswordEncoderFactories}
* ————————————————
* 原文链接:https://blog.csdn.net/alinyua/article/details/80219500
* @return PasswordEncoder
*/
@Bean
static PasswordEncoder passwordEncoder() {
// 数据库存储格式,{bcrypt}xxxx ,例如 '{bcrypt}$2a$10$hpROwWyEu4AbMJpFF2jLBusGdRnOK2VktGy3VVnrsmkC98TFWnJ.K'
val idForEncode = "bcrypt";
return new DelegatingPasswordEncoder(idForEncode,Map.of(
idForEncode, new BCryptPasswordEncoder(),
"SHA-1", new MessageDigestPasswordEncoder("SHA-1")));
// return new BCryptPasswordEncoder();
}
/**
* 生成加密密码demo
* @param args
*/
public static void main(String[] args) {
//System.out.println(new BCryptPasswordEncoder().encode("123456"));
System.out.println(new MessageDigestPasswordEncoder("SHA-1").encode("123456"));
}
/**
* 设置登录地址入口:http://localhost:8080/authorize/login
* 请求方式POST application/json
* @return
* @throws Exception
*/
private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
// 登录成功后置处理器。
filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.getWriter().println("LoginSuccess: "+
objectMapper.writeValueAsString(authentication));
});
// 登录失败后置处理器
filter.setAuthenticationFailureHandler((request, response, exception) -> {
log.error("认证失败", exception);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().println("LoginFailure!");
});
// 获得AuthenticationManager
filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
// 设置登登录请求地址
filter.setFilterProcessesUrl("/authorize/login");
return filter;
}
// /**
// * 放行不需要认证的URL
// * 另一种放行方法是在HttpSecurity 对url添加permitAll()放行(官方推荐)
// * @return
// */
// @Bean
// public WebSecurityCustomizer webSecurityCustomizer() {
// return (web) -> web.ignoring().antMatchers("/api/say")
// // 放行通用的css、js等静态资源位置
// .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
// }
}
WebMvcConfig 配置
package com.example.uaa.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.webjars.WebJarAssetLocator;
import java.util.Map;
/**
* 前端资源配置
*/
@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
//D:\Code\apache-maven-3.6.3\repo\org\webjars\bootstrap\4.5.0\bootstrap-4.5.0.jar!\META-INF\resources\webjars\bootstrap\4.5.0\js\bootstrap.js
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.setOrder(1);
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// “/login”对应访问的登录页URL, “login”对应login.html具体的视图
registry.addViewController("/login").setViewName("login");
registry.addViewController("/").setViewName("index");
// swagger 地址
// registry.addViewController( "/swagger-ui/")
// .setViewName("forward:/swagger-ui/index.html");
// 优先级最高
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
身份认证过滤器RestAuthenticationFilter
package com.example.uaa.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
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;
/**
* 自定义登录过滤器,使用json格式传参数。返回json数据
*/
@RequiredArgsConstructor
public class RestAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final ObjectMapper objectMapper;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("不支持的请求方法: " + request.getMethod());
}
UsernamePasswordAuthenticationToken authRequest;
try {
var jsonNode = objectMapper.readTree(request.getInputStream());
String username = jsonNode.get("username").textValue();
String password = jsonNode.get("password").textValue();
authRequest = new UsernamePasswordAuthenticationToken(username, password);
} catch (IOException e) {
e.printStackTrace();
throw new BadCredentialsException("参数解析出错!");
}
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// AuthenticationManager 与 UserDetailService之间的关,以及如何使用UserDetailService来完成认证请看:
// https://docs.spring.io/spring-security/reference/5.7/servlet/authentication/passwords/dao-authentication-provider.html
return this.getAuthenticationManager().authenticate(authRequest);
}
}