Article Directory
- foreword
- Memory version (memory)
- Database version (jdbc)
- Custom Login - Single (custom-login-single)
- Custom login - front and rear separated
- Change login to json login (custom-login-json)
- certified
- Authorization/Access Control (custom-auth-control)
- Warehouse Address
foreword
As the first article, this article will use examples to illustrate the usage of Spring Security in production and expand its functions. Each solution will have a complete example code, and the code warehouse will be posted at the end of the article.
The theory involved in this article is less, mainly with examples.
Memory version (memory)
This version has no technology, and it can be used by introducing dependencies.
direct import
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- After introducing dependencies, you can start the project directly
- A random password will be generated in the console, and the default username is
user
Using generated security password: 2233be62-e65f-489b-a52c-4bba21bcfd14
-
set user password
-
It can be set through application.yml, or injected through config, obviously it is more convenient to use yml
spring: security: # 配置默认的 InMemoryUserDetailsManager 的用户账号与密码。 user: name: ali password: 123456 roles: admin
-
There are two ways to write directly through the code, and the two can also be mixed
Method 1: direct injection
InMemoryUserDetailsManager
@Configuration @EnableWebSecurity public class SecurityWebCofnig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("ali") .password(passwordEncoder().encode("123456")) .roles("admin"); } }
Method 2: Interface-oriented method
@Configuration @EnableWebSecurity public class SecurityWebCofnig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("ali") .password(passwordEncoder().encode("123456")) .roles("admin"); auth.userDetailsService(userDetailsService); } } @Component public class CustomUserServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return new User("ali2", passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
-
Database version (jdbc)
This version, on the basis of the memory version, has upgraded the function of user verification, extending from the built-in user to the database, and it is also a relatively limited version.
Then the focus here is the implementation userDetailService
method, UserDetail
just return a subclass, and other login authentication and redirection are all done by security.
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
@Data
@TableName("role")
public class Role implements Serializable {
private static final long serialVersionUID = -27787294406430777L;
@TableField("id")
private Integer id;
@TableField("name")
private String name;
@TableField("code")
private String code;
@TableField("status")
private Integer status;
@TableField("deleted")
private Integer deleted;
}
@Data
@TableName("user")
public class SysUser {
private String id;
private String username;
private String realName;
private String password;
@TableField(exist = false)
private String[] roles;
}
@Data
@TableName("user_role")
public class UserRole implements Serializable {
private static final long serialVersionUID = 180339547857105479L;
@TableField("id")
private Integer id;
@TableField("role_id")
private Integer roleId;
@TableField("user_id")
private Integer userId;
}
public interface SysUserService {
SysUser getByUsername(String username);
}
@Service
@AllArgsConstructor
public class SysUserServiceImpl implements SysUserService {
private SysUserRepository sysUserRepository;
private UserRoleRepository userRoleRepository;
private RoleRepository roleRepository;
@Override
public SysUser getByUsername(String username) {
SysUser user = sysUserRepository.getByUsername(username);
if (user == null) {
return null;
}
List<UserRole> roles = userRoleRepository.getByUserId(user.getId());
if (roles.isEmpty()) {
return user;
}
List<Integer> roleIds = roles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
user.setRoles(roleRepository.listByIds(roleIds).stream().map(Role::getCode).collect(Collectors.toList()));
return user;
}
}
User-defined security query method
@Component
@AllArgsConstructor
public class CustomUserServiceImpl implements UserDetailsService {
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if (sysUser == null) {
return null;
}
return new User(sysUser.getUsername(), sysUser.getPassword(), AuthorityUtils.createAuthorityList(sysUser.getRoles()));
}
}
When injecting here, users of the memory version are retained.
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
}
I use Mybatis plus, add mapperScan here
@MapperScan("com.liry.security.repository.mapper")
@SpringBootApplication
public class JdbcApp {
public static void main(String[] args) {
SpringApplication.run(JdbcApp.class);
}
}
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/ali?useSSl=false&zeroDateTimeBehavior=CONVERT_TO_NULL&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: SsoHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
main:
allow-bean-definition-overriding: true
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
id-type: AUTO
configuration:
# 日志打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
Custom Login - Single (custom-login-single)
This version upgrades the login custom function on the database version, and you can set your own login page, home page, login processing api, and callback processing for successful and failed logins according to the project;
Digression: The advantage of not separating the front and back is rapid development and easy deployment, but its disadvantages are also obvious. With the iterative project, it will become larger and larger. The mainstream is still distributed, and single projects are rare now.
So here are the things to note:
- After the front-end framework is introduced, it needs to be configured in application.yml (I use themleaf here)
- Inheritance
WebSecurityConfigurerAdapter
class completes custom configuration- Use settings for successful login page
defaultSuccessUrl
, don't usesuccessForwardUrl
- You can define a successful post-processor and a failure processor, these two will be called back after authentication, and you can do some business expansion
- Use settings for successful login page
- The configuration
loginProcessingUrl
is not to transfer the processing request to the interface you defined, but to modify the api address inside the security
Notice:
loginProcessingUrl
: The api for login processing, this is just to define the api, not to change the authentication to custom, or to be controlled by security
defaultSuccessUrl
: The jump address after successful login, there is another one successForwardUrl
, but it can only be used defaultSuccessUrl
, and successForwardUrl
a 405 error will appear when using it
permitAll
: allow all request methods
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
http
.formLogin()
// 自定义登录页
.loginPage("/login.html")
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 不能写:successForwardUrl("/index.html"),会报405
.defaultSuccessUrl("/index.html")
// 登录失败转发到哪个页面
.failureForwardUrl("/login.html?error=true")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
Login failure callback
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
// 处理失败的后置操作
}
}
Successful login callback
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
// 处理成功的一个后置操作
}
}
test controller
@Controller
public class LoginController {
@GetMapping("/login.html")
public String loginPage() {
return "login";
}
@GetMapping("/index.html")
public String index() {
return "index";
}
}
Tested login page (login.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<div>
<!-- 这里action对应的是在-->
<form action="/loginDeal" method="POST" >
用户名<input placeholder="输入用户名" name="username"></br>
密 码<input placeholder="输入密码" name="pwd" type="password"></br>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
test home page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h2>首页</h2>
</body>
</html>
Custom login - front and rear separated
Unlike a single project, the scalability of front-to-back separation is greatly increased, which means that it will face more problems, such as cross-domain and session consistency
-
Customize the method
CustomUserServiceImpl
of loadingUserDetailsService
user information -
Custom
LoginSuccessHandler/LoginFailureHandler
authentication callback processing method (successful login, store user information, response serialization) -
Create a cache interface
CacheManager
unified cache -
inherit
WebSecurityConfigurerAdapter
Configuration:
corsFilter(跨域)
userDetailService(用户信息)
passwordEncoder(加解密)
securityContextRepository(认证信息管理)
AuthenticationEntryPoint(响应序列号)
-
There are many ways to deal with session consistency. Different people have different opinions. There are advantages and disadvantages. Redis is used here. If there is no need for a large environment, one front-end and one back-end are also possible.
Cache management, redis used here;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
/**
* @author ALI
* @since 2023/6/4
*/
@Component
public class CacheManager {
private static final int TIME_OUT = 4;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public <T> T get(String key, Class<T> clazz) {
Object value = get(key);
if (value == null) {
return null;
}
return (T) value;
}
public void set(String key, Object value) {
if (value == null) {
redisTemplate.delete(key);
return;
}
redisTemplate.opsForValue().set(key, value, TIME_OUT, TimeUnit.HOURS);
}
public void set(String key, Object value, Long timeOut, TimeUnit timeUnit) {
if (value == null) {
redisTemplate.delete(key);
return;
}
redisTemplate.opsForValue().set(key, value, timeOut, timeUnit);
}
public boolean containsKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public long getExpire(String key) {
Long expire = redisTemplate.getExpire(key);
if (expire == null) {
return 0L;
}
return expire;
}
}
/**
* 认证的常量
* @author ALI
* @since 2023/6/10
*/
public class AuthConstant {
public static final String LOGIN_PRE = "login:";
public static final String CAPTCHA_PRE = "captcha:";
public static String buildLoginKey(String key) {
return LOGIN_PRE + key;
}
public static String buildCaptchaKey(String key) {
return CAPTCHA_PRE + key;
}
}
@Data
public class CustomUser implements UserDetails {
private static final long serialVersionUID = 5469888959861441262L;
protected String userId;
protected String password;
protected String username;
protected Collection<? extends GrantedAuthority> authorities;
public CustomUser() {
}
public CustomUser(SysUser sysUser) {
this.userId = sysUser.getId();
this.username = sysUser.getUsername();
this.password = sysUser.getPassword();
if (!CollectionUtils.isEmpty(sysUser.getRoles())) {
this.authorities = sysUser.getRoles().stream().map(d -> new SimpleGrantedAuthority("ROLE_" + d)).collect(Collectors.toList());
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@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;
}
}
response object
/**
* 返回结果对象
*
* @author 李瑞益
* @since 2019/9/25
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ResponseData<T> implements Serializable {
public static final String SUCCESS = "success";
public static final String FAILED = "failed";
private static final long serialVersionUID = -4304353934293881342L;
/** 处理结果 */
private boolean status;
/** 信息 */
private String message;
/** 状态编码 */
private String code;
/** 数据对象 */
private T data;
public ResponseData(boolean status, T data) {
this.status = status;
this.data = data;
this.code = status ? SUCCESS : FAILED;
}
public ResponseData(boolean status, T data, String message) {
this.status = status;
this.data = data;
this.message = message;
this.code = status ? SUCCESS : FAILED;
}
public ResponseData(Throwable e) {
this.status = false;
this.message = e.getMessage();
this.code = FAILED;
}
public static ResponseData<Object> ok() {
return new ResponseData<>(true, null);
}
public static ResponseData<Object> failed() {
return new ResponseData<>(false, null);
}
public static ResponseData<Object> failed(Throwable e) {
return new ResponseData<>(false, null, e.getMessage());
}
public static ResponseData<Object> failed(String message) {
return new ResponseData<>(false, null, message);
}
}
/**
* 主要用来做响应体序列化
* @author ALI
* @since 2023/6/4
*/
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
ResponseData<Object> result = ResponseData.failed(e.getMessage());
PrintWriter out = httpServletResponse.getWriter();
out.write(JSON.toJSONString(result));
out.flush();
out.close();
}
}
Note that:
Our previous codes are all services based on a single architecture, so when configuring SecurityWebConfig
, we will configure login pages, error pages, etc., but these things are no longer needed after the front and back are separated.
Session Consistency Scheme
When it comes to the session consistency scheme, here are three schemes, which are gradually developed.
Option One
Use redis cache directly in the callback method (custom-login)
This solution uses the callback processor of successful login in security to set user information to redis, then add a request header interceptor to intercept the token in the request header, get the user information in redis through token, and then set it to the context of security SecurityContextHolder
That's fine; the disadvantage of this solution is that it will keep two caches, and the amount of code is also large
- Customize the method
CustomUserServiceImpl
of loadingUserDetailsService
user information - Custom
LoginSuccessHandler/LoginFailureHandler
authentication callback processing method (successful login, store user information, response serialization) - Create a cache interface
CacheManager
unified cache - Custom
CustomizeAuthenticationEntryPoint
implementation ofAuthenticationEntryPoint
the serialization method - Custom
CustomHeaderAuthFilter
inheritanceBasicAuthenticationFilter
to complete the processing of the request header token LoginSuccessHandler
Add user caching logic- Inherit
WebSecurityConfigurerAdapter
configuration custom login
Notice:
-
If you use it , don’t set it when
BasicAuthenticationFilter
configuring , or you won’t go through our custom FilterWebSecurityConfigurerAdapter
http.httpBasic()
-
This method will be separated from security. In customization
CustomHeaderAuthFilter
, you need to judge the login API and ignore API to avoid being intercepted by yourself
LoginSuccessHandler: After the user logs in successfully, set the user information to the cache and serialize json to the front end
/**
* 登录成功处理器
* 序列化处理
*
* @author ALI
* @since 2023/6/1
*/
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private CacheManager cacheManager;
public LoginSuccessHandler(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
// 将成功后的会话id设置到响应,以便在链里的过滤器能够拿到
String token = UUID.randomUUID().toString();
httpServletResponse.setHeader(HttpHeaders.AUTHORIZATION, token);
// 设置响应的格式
httpServletResponse.setContentType("application/json;charset=utf-8");
ResponseData<CustomUser> result = new ResponseData<>();
CustomUser user = (CustomUser) authentication.getPrincipal();
cacheManager.set(AuthConstant.buildLoginKey(token), user);
UserView temp = new UserView(user, token);
result.setData(temp);
PrintWriter writer = httpServletResponse.getWriter();
writer.write(JSON.toJSONString(result));
writer.flush();
}
}
CustomHeaderAuthFilter: A custom request header filter that intercepts the token header in the request header. This is equivalent to our own initiative to authenticate. Then this filter must be before the filter. After authentication, you need to pass the UsernamePasswordAuthenticationFilter
authentication The information is set in the security context, and because our filter is a bit independent from the security, we need to synchronize the SecurityWebConfig
ignored api configured in it;
It should be noted that all of them try catch
are captured here, so that any exceptions ExceptionTranslationFilter
will be handled by
/**
* 主要用来拦截token的
* <p>
* 这里构造器我需要注入authenticationManager ,但是这个类在SecurityConfig里注入,所有我只有在用到的地方手动注入
*
* @author ALI
* @since 2023/6/4
*/
public class CustomHeaderAuthFilter extends BasicAuthenticationFilter {
private CacheManager cacheManager;
public CustomHeaderAuthFilter(AuthenticationManager authenticationManager,
AuthenticationEntryPoint authenticationEntryPoint, CacheManager cacheManager) {
super(authenticationManager, authenticationEntryPoint);
this.cacheManager = cacheManager;
}
private void doParse(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
try {
// 这个步骤是将redis的信息设置到security上下文
if (header.startsWith("Bearer")) {
String token = header.replace("Bearer ", "");
CustomUser user = cacheManager.get(AuthConstant.buildLoginKey(token), CustomUser.class);
if (user == null) {
throw new AccountExpiredException("token无效!");
}
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
// 设置的上下文
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (Exception e) {
// 抛出 AuthenticationException AccessDeniedException 两个类型的异常给 ExceptionTranslationFilter
throw new AccountExpiredException("登录失败!");
}
chain.doFilter(request, response);
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
String authorization = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);
// 无token,和登录的走默认的逻辑
// 还有被忽略的api
if (httpServletRequest.getRequestURI().contains("/login") || httpServletRequest.getRequestURI().contains("/test")) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
this.doParse(httpServletRequest, httpServletResponse, filterChain);
}
}
SecurityWebConfig: Here is the configuration of security. It should be noted that don't forget to inject the custom class and configure it
/**
* security认证配置
*
* @author ALI
* @since 2023/5/29
*/
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CacheManager cacheManager;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public org.springframework.web.filter.CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}
/**
* 跨域设置
*/
private CorsConfigurationSource corsConfigurationSource() {
org.springframework.web.cors.UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// 允许向该服务器提交请求的URI,* 表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// 允许访问的头信息,* 表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,* 表示全部允许
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler(cacheManager))
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
http.addFilterBefore(
new CustomHeaderAuthFilter(authenticationManager(), new CustomizeAuthenticationEntryPoint(), cacheManager),
UsernamePasswordAuthenticationFilter.class);
}
}
Option II
Rewrite the cache provider class inside Spring Security SecurityContextRepository
(custom-login2)
This solution starts from the perspective of the source code framework, and the replacement execution bean
has achieved that the final access to user information is the same cache space. It is very simple in principle, but it requires some source code knowledge, and the business requirements are changeable. Although it supports security
expansion , but the learning cost is not low, so you need to look at the project from a global perspective, but it is very comfortable to use, and the amount of code is very small, if you want to check the internal use, you can check it SecurityContextPersistenceFilter
.
- Customize the method
CustomUserServiceImpl
of loadingUserDetailsService
user information - Custom
LoginSuccessHandler/LoginFailureHandler
authentication callback processing method (response serialization) - Create a cache interface
CacheManager
unified cache - Custom
CustomizeAuthenticationEntryPoint
implementation ofAuthenticationEntryPoint
the serialization method - Custom
CustomSecurityContextRepository
inheritanceSecurityContextRepository
overrides its 3 information access methods - Inherit
WebSecurityConfigurerAdapter
configuration custom login
/**
* 自定义的session存储器
*
* @author ALI
* @since 2023/6/4
*/
@Component
public class CustomSecurityContextRepository implements SecurityContextRepository {
@Autowired
private CacheManager cacheManager;
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.isBlank(token)) {
return generateNewContext();
}
token = token.replace("Bearer ", "");
SecurityContextImpl s = cacheManager.get(AuthConstant.buildLoginKey(token), SecurityContextImpl.class);
if (s == null) {
return generateNewContext();
}
return s;
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.isBlank(token)) {
token = response.getHeader(HttpHeaders.AUTHORIZATION);
}
if (StringUtils.isBlank(token)) {
return;
}
token = token.replace("Bearer ", "");
// 登录成功和失败的回调(LoginSuccessHandler,LoginFailureHandler)是在UsernamePasswordAuthenticationFilter过滤器里执行的
// 而这里的认证信息缓存是在`SecurityContextPersistenceFilter`的doFilter后执行的
// `SecurityContextPersistenceFilter`的顺序比`UsernamePasswordAuthenticationFilter`的顺序小,
// 那么doFilter之后的方法就晚与LoginSuccessHandler,LoginFailureHandler
cacheManager.set(AuthConstant.buildLoginKey(token), context);
}
@Override
public boolean containsContext(HttpServletRequest request) {
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.isBlank(token)) {
return false;
}
token = token.replace("Bearer ", "");
return cacheManager.containsKey(AuthConstant.buildLoginKey(token));
}
protected SecurityContext generateNewContext() {
return SecurityContextHolder.createEmptyContext();
}
}
security configuration
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomSecurityContextRepository securityContextRepository;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public org.springframework.web.filter.CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}
/**
* 跨域设置
*/
private CorsConfigurationSource corsConfigurationSource() {
org.springframework.web.cors.UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// 允许向该服务器提交请求的URI,* 表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// 允许访问的头信息,* 表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,* 表示全部允许
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
}
}
Why use this: http.setSharedObject
?
Look at SecurityContextConfigurer
the configuration class, it will http.getSharedObject(SecurityContextRepository.class)
obtain the corresponding implementation class, which is internally implemented by Map, so when we http.setSharedObject
set our custom repository, we can rewrite it; by default, , http.getSharedObject(SecurityContextRepository.class) = null
so, it is directly new HttpSessionSecurityContextRepository
set toSecurityContextPersistenceFilter
third solution
use Spring session
(custom-login3)
This solution uses spring components spring session
, which is also a native spring, and it is also aimed at session management; again, different people have different opinions.
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
and then annotate@SpringBootApplication
Then it's done. This method saves the most effort. After all, this is a component produced by spring. It is a functional upgrade for the spring framework. This method does not require you to set tokens and other things.
Why not use JWT
In fact, there is another way to solve session consistency, but I don't really support this way, that is, JWT
it is defined as a stateless login, and its core meaning is that the JWT
generated information is the login information, so as long as With this information, the login data can JWT
be obtained through this information.
It's really good, but there are a few things I don't really like:
- There is a part of the information generated by JWT that can be reversed to extract user information;
- JWT cannot actively expire, so this cannot be launched in the true sense, then to achieve expiration, it must be cached, then this is the same as the second solution, but the second solution does not need to write decryption, verify the legitimacy of the JWT, and does not need to go Adapt security;
- This is similar to the above, that is, if the business requires login restrictions, such as only one login with the same account, then JWT still needs to be cached;
The above 3 points are enough for me not to support using JWT.
Change login to json login (custom-login-json)
Security’s default login is all POST + form-data
. If you use json, you can’t get the parameters, but the front-end projects generally do unified interception processing. Of course, you can also let the front-end change the login to formData request.
There are several points to note about this login:
-
The default login authentication
UsernamePasswordAuthenticationFilter
is done by , its value is not in json format, so we rewrite its authentication methodattemptAuthentication
; -
Set our custom
UsernamePasswordAuthenticationFilter
settings toHttpSecurity
, because it is a replacement, so we need toUsernamePasswordAuthenticationFilter
copy the relevant configuration, such as the login parameter name, login processing api, login success processor and failure processor ( this is very important ); -
PasswordEncoder
Password matching is to pass in unencrypted (passed by the front end) and encrypted (saved by the back end) passwords, so if the password is encrypted, it needs to be decrypted here;
Customize UsernamePasswordAuthenticationFilter
the authentication of json
/**
* 自定义json认证
* @author ALI
* @since 2023/6/7
*/
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try (InputStream inputStream = request.getInputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> loginRequest = objectMapper.readValue(inputStream, Map.class);
String username = loginRequest.get(super.getUsernameParameter());
String password = loginRequest.get(super.getPasswordParameter());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Add custom configurationHttpSecurity
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
// 开启认证
.authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
// 将我们自定义过滤器加入到原来UsernamePasswordAuthenticationFilter的前面
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 自定义的customUsernamePasswordAuthenticationFilter
* 需要同步在HttpSecurity里的配置
*/
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() throws Exception {
CustomUsernamePasswordAuthenticationFilter result = new CustomUsernamePasswordAuthenticationFilter();
result.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(loginApi));
result.setAuthenticationManager(this.authenticationManager());
result.setUsernameParameter(usernameParameter);
result.setPasswordParameter(passwordParameter);
result.setAuthenticationSuccessHandler(new LoginSuccessHandler());
result.setAuthenticationFailureHandler(new LoginFailureHandler());
return result;
}
Then addFilterBefore
this method is provided by security for us to expand. If we customize one UsernamePasswordAuthenticationFilter
and put it in front, after our authentication is passed, we will not continue to go through the following filters, and our coverage is completed.
certified
Here, the custom-login-json project is used as the basic project.
Because if you want to attack your website, the first possibility is to try to crack the password. If your transmission is not secure, it will be intercepted, so you need to encrypt it. Moreover, if the attacker uses brute force to attack, the password requires It needs to be complicated, increase the range of exhaustion of the attacker, reduce the hit rate, and increase the number of failures of the account, as well as the ip limit. At the same time, you can also set the verification code to increase the difficulty of cracking.
Password encrypted login (custom-auth)
Network security is also something that our programmers should consider, so here we do a password encryption and decryption, here we use the second scheme (custom-login2) as the basic project, but the following scheme is not limited to a certain scheme, but It is applicable to all security items.
encryption tool
The RSA asymmetric encryption algorithm is used here, and both encryption and decryption require the use of public and secret keys, which is highly secure.
/**
* rsa加密工具简化版
*
* @author ALI
* @date 2021-09-25 15:44
*/
public class RsaUtil {
private static final String RSA_ALGORITHM = "RSA";
private static final String AES_ALGORITHM = "AES";
private RsaUtil() {
}
/**
* 生成密钥对
*
* @param passKey 关键密码
* @return 密钥对
*/
public static KeyPair genratorKeyPair(String passKey) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
SecureRandom secureRandom = new SecureRandom(passKey.getBytes());
secureRandom.setSeed(passKey.getBytes());
keyPairGenerator.initialize(2048, secureRandom);
return keyPairGenerator.generateKeyPair();
}
/**
* 加密密码
*
* @param password 密码
* @param publicKey 公钥
* @return 加密后的密文
*/
public static byte[] encrypt(PublicKey publicKey, byte[] password) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(password);
} catch (Exception e) {
throw new RuntimeException("RSA加密失败(RSA encrypt failed.)");
}
}
/**
* 解密密码
*
* @param encryptPassword 加密的密码
* @param privateKey 私钥
* @return 解密后的明文
*/
public static byte[] decrypt(PrivateKey privateKey, byte[] encryptPassword) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptPassword);
} catch (Exception e) {
throw new RuntimeException("RSA解密失败(RSA encrypt failed.)");
}
}
/**
* 密钥Base64
*
* @param privateKey 密钥
* @return 结果
*/
public static String getPrivateBase64(PrivateKey privateKey) {
return Base64.getEncoder().encodeToString(privateKey.getEncoded());
}
/**
* 公钥Base64
*
* @param publicKey 公钥
* @return 结果
*/
public static String getPublicBase64(PublicKey publicKey) {
return Base64.getEncoder().encodeToString(publicKey.getEncoded());
}
/**
* 根据公钥字符串获取公钥对象
*
* @param publicKeyString 公钥字符串
* @return 结果
*/
public static PublicKey getPublicKey(String publicKeyString)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] decode = Base64.getDecoder().decode(publicKeyString);
return KeyFactory.getInstance(RSA_ALGORITHM).generatePublic(new X509EncodedKeySpec(decode));
}
/**
* 根据密钥字符串获取密钥对象
*
* @param privateKeyString 密钥字符串
* @return 结果
*/
public static PrivateKey getPrivateKey(String privateKeyString)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] decode = Base64.getDecoder().decode(privateKeyString);
return KeyFactory.getInstance(RSA_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(decode));
}
/**
* 对称加密AES 对称key生成
*
* @param passKey 关键密码
* @return 生成aes的key
* @throws NoSuchAlgorithmException 算法找不到异常
*/
public static SecretKey aesKey(String passKey) throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(AES_ALGORITHM);
SecureRandom secureRandom = new SecureRandom();
secureRandom.setSeed(passKey.getBytes());
keyGenerator.init(secureRandom);
return keyGenerator.generateKey();
}
/**
* @param mode 加解密模式:Cipher.ENCRYPT_MODE / Cipher.DECRYPT_MODE
* @param secretKey 对称key
* @param password 执行的密码
*/
public static byte[] aes(int mode, SecretKey secretKey, byte[] password) {
try {
Cipher instance = Cipher.getInstance(AES_ALGORITHM);
instance.init(mode, secretKey);
return instance.doFinal(password);
} catch (Exception e) {
throw new RuntimeException(String.format("AES执行失败,Cipher.mode:%s(AES encrypt failed.)", mode));
}
}
public static void main(String[] args) throws Exception {
String passkey = "dd";
KeyPair dd = genratorKeyPair(passkey);
String pu = new String(Base64.getEncoder().encode(dd.getPublic().getEncoded()));
String en = new String(Base64.getEncoder().encode(dd.getPrivate().getEncoded()));
System.out.println("publicKey:\n" + pu);
System.out.println("private:\n" + en);
// 加解密方案1:RSA + AES双重加密
// 1. AES加密
SecretKey key = aesKey(passkey);
byte[] aesEn = aes(Cipher.ENCRYPT_MODE, key, "123456".getBytes());
// 2. 通过RSA公钥加密密码
byte[] rsaEn = encrypt(dd.getPublic(), aesEn);
// 3. 通过RSA私钥解密密码
byte[] rsaDe = decrypt(dd.getPrivate(), rsaEn);
// 4. 再同AES解密
byte[] aesDe = aes(Cipher.DECRYPT_MODE, key, rsaDe);
System.out.println("两重解密:" + new String(aesDe));
// 加解密方案2:RSA加密
byte[] encrypt = encrypt(RsaUtil.getPublicKey(pu), "123456".getBytes());
byte[] decrypt = decrypt(RsaUtil.getPrivateKey(en), encrypt);
System.out.println("RSA解密:" + new String(decrypt));
}
}
generate key pair
Execute RsaUtil
the main method, generate publicKey
and privateKey
, and then privateKey
save it in the backend and publicKey
give it to the frontend, and then when the frontend passes the password to the backend, it first passes RSA
and publicKey
encrypts;
Put the private key in the file: privateKey.pem
, use pem
or der
suffix format file storage, otherwise there will be problems in parsing
read private key
SecurityWebConfig
Add a constructor to read the private key during initialization (this method can be used for packaging and can be used with confidence);
privateKey is set to public and can be called anywhere in the project.
public static PrivateKey privateKey;
public SecurityWebConfig() throws Exception {
try(InputStream is = this.getClass().getClassLoader().getResourceAsStream("privateKey.pem")) {
if (is == null) {
throw new RuntimeException("没有读取的密钥!!!");
}
byte[] data = new byte[2048];
int length = is.read(data);
String privateKeyString = new String(data, 0, length);
privateKey = RsaUtil.getPrivateKey(privateKeyString.trim());
}
}
filter decryption
CustomUsernamePasswordAuthenticationFilter
Decrypt the password in the custom one, and then set it authenticationToken
in, because the latter verification method matches
needs to match the password before encryption and the password after encryption;
@Slf4j
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static String decondePassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] decode = Base64.getDecoder().decode(password.getBytes());
return new String(RsaUtil.decrypt(SecurityWebConfig.privateKey, decode));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try (InputStream inputStream = request.getInputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> loginRequest = objectMapper.readValue(inputStream, Map.class);
String username = loginRequest.get(super.getUsernameParameter());
String password = loginRequest.get(super.getPasswordParameter());
try {
password = decondePassword(password);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
log.error("密码解密失败!!!!", e);
password = null;
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 使用 AuthenticationManager 进行身份验证
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Of course, the decryption process can also be decrypted at the place that really matches, and the rewriting org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks
method has the same logic, so there is no need to write code.
Add custom filter to security
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
// 自定义过滤器加入security
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
Front-end configuration and login
Here use the template project (), after git down, install the encryption tool
npm install jsencrypt
npm install
-
Modify
axios.js
the endAxios.interceptors.response.
to the following:Axios.interceptors.response.use((res) => { if (res.config.direct) { return res.data } return Promise.resolve(res) }, (error) => { Message.error(error.response.data) return Promise.reject(error.response.data) })
-
Add login method
export function login (data, success, error) { http.post('/loginDeal', data, success, error) }
-
Configure the key into an environment variable
Here only configuration is required
dev.env.js
in andprod.env.js
in, dev is the development environment, and prod is the production environment.Note: The variable value here is marked with a double quote and a single quote, so that when used, it will not be
webpack
directly replaced at compile time and cause a syntax error.module.exports = merge(prodEnv, { NODE_ENV: '"development"', PUBLIC_KEY: '"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnQRntjLw0wC4xPki/Tvf+7esQwf2PoCmvb8oKypvssevr8LK74CF/Yh0AjMvmoAlr0UXm5VK4B2edmvLwLTeFjAU8zXdNLlhC7YDUpc/vEZFPhh2jvUMjOe0LAJb+FOv5oMGpAxuj8PC9Cz4L05T/gOI7w8FPwCJjXJacWPhhSAK+dViXHLZVqNeIo4YRUT8C2s5e+vz03FByd511YaydVTbBGRB7+QVFJ5f6Rt9buxn9gDK5CcZ27ScQvdc88w9NF0bfmNRh8xec3Cz9uMyRVhy5d3pJM9a6jTEHcbOTapUAjssq2cVr+qx5DGv87u4I8qKqJQIhvu40Vd3foR0JQIDAQAB"' })
-
Create
login.vue
(I simply configured it here)For the login form, I separate the password field entered by the user from the password field transmitted to the backend, in order to improve the user experience.
<template> <div> <el-row> <el-col :span="6"> <el-form :model="form" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm"> <el-form-item label="用户名" prop="username"> <el-input type="text" v-model="form.username"></el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input type="password" v-model="form.password"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('ruleForm')">提交</el-button> </el-form-item> </el-form> </el-col> </el-row> </div> </template> <script> import {loginApi} from '../api' import {JSEncrypt} from 'jsencrypt' export default { name: 'login', data () { return { form: { username: '', password: '', pwd: '' }, rules: { password: [ {required: true, trigger: 'blur', message: '请输入密码'} ], username: [ {required: true, trigger: 'blur', message: '请输入用户名'} ] } } }, methods: { submitForm (formName) { this.$refs[formName].validate((valid) => { if (valid) { // 加密密码 this.form.pwd = this.encryptedData(process.env.PUBLIC_KEY, this.form.password) this.form.password = null loginApi.login(this.form, success => { this.$message.success('登录成功') // 存储token信息,和用户信息 // 跳转指定页面 console.log(success) }) } else { this.$message.info('表单验证失败') return false } }) }, // 加密密码 encryptedData (publicKey, data) { // 新建JSEncrypt对象 let encryptor = new JSEncrypt() // 设置公钥 encryptor.setPublicKey(publicKey) // 加密数据 return encryptor.encrypt(data) } } } </script> <style scoped> </style>
-
route/index.js
Add routing{ path: '/login', name: 'login', component: login }
-
npm run dev
-
browser
http://localhost:8080/#/login
result
Captcha login (custom-auth-captcha)
Continue to complete the above example of encryption and decryption.
The principle is that the backend randomly generates a series of numbers, saves them, and then generates a picture for the frontend, or randomizes a simple calculation formula, such as: , then calculates the value, 1+8=
saves it, and then generates a picture for the frontend with this formula. The focus is on the before and after How to connect, I use it here sessionId
.
Note that the verification code cannot be used all the time, and the randomness of the verification code needs to be provided through time and verification strategies.
generate verification code
The tools used by the captcha tool here Hutool
,
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>5.8.15</version>
</dependency>
Here I inject the tool generated by the algorithm as a singleton, and then use it during verification, and then the verification code expires in 2 minutes
/**
* 认证
*
* @author ALI
* @since 2023/6/10
*/
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private CacheManager cacheManager;
@Autowired
private MathGenerator mathGenerator;
@Override
public void captcha(HttpServletRequest request, HttpServletResponse response) {
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 45, 4, 4);
// 自定义验证码内容为四则运算方式
captcha.setGenerator(mathGenerator);
// 重新生成code
captcha.createCode();
String code = captcha.getCode();
String id = request.getSession().getId();
cacheManager.set(AuthConstant.buildCaptchaKey(id), code, 2L, TimeUnit.MINUTES);
try (ServletOutputStream os = response.getOutputStream()) {
// 这里通过base64加密再返回给前端,前端就不用处理了
os.write(Base64.getEncoder().encode(captcha.getImageBytes()));
} catch (IOException e) {
throw new RuntimeException("验证码异常!");
}
}
}
I didn’t use the hutool tool directly here captcha.write(os);
, because if I do this, it will write the byte stream directly to the front end, and then the front end needs to process it again, so here I get the byte, encrypt it again, and Base64
then Written for the front-end, so that the front-end only needs to spell one before the result data:image/png;base64,
to use it.
Provide verification code interface
@GetMapping("/login/captcha")
public String captcha(HttpServletRequest request, HttpServletResponse response) {
authService.captcha(request, response);
return null;
}
Create captcha filter
Customize a filter and add it to the security interception chain.
The custom verification code filter here, inherited OncePerRequestFilter
, will only go once, of course, it only needs to go once, and delete the key after the verification is successful;
/**
* 自定义的验证码过滤器
*
* @author ALI
* @since 2023/6/10
*/
public class CustomCaptchaFilter extends OncePerRequestFilter {
public String loginApi;
private CacheManager cacheManager;
private MathGenerator mathGenerator;
private AuthenticationEntryPoint entryPoint;
public CustomCaptchaFilter(CacheManager cacheManager, MathGenerator mathGenerator, AuthenticationEntryPoint authenticationEntryPoint,
String loginApi) {
this.cacheManager = cacheManager;
this.mathGenerator = mathGenerator;
this.entryPoint = authenticationEntryPoint;
this.loginApi = loginApi;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getRequestURI().equals(loginApi)) {
String id = request.getSession().getId();
String key = AuthConstant.buildCaptchaKey(id);
Object match = cacheManager.get(key);
if (match == null) {
entryPoint.commence(request, response, new BadCredentialsException("验证码过期!"));
return;
}
// 注意这里的验证码参数不是从body里获取的
if (!mathGenerator.verify(match.toString(), request.getParameter(AuthConstant.CAPTCHA))) {
entryPoint.commence(request, response, new BadCredentialsException("验证码验证错误!"));
return;
}
// 验证成功删除
cacheManager.delete(key);
}
filterChain.doFilter(request, response);
}
}
Add filter to security
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
private static final String loginApi = "/loginDeal";
private static final String usernameParameter = "username";
private static final String passwordParameter = "pwd";
public static PrivateKey privateKey;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomSecurityContextRepository securityContextRepository;
@Autowired
private CacheManager cacheManager;
public SecurityWebConfig() throws Exception {
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("privateKey.pem")) {
if (is == null) {
throw new RuntimeException("没有读取的密钥!!!");
}
byte[] data = new byte[2048];
int length = is.read(data);
String privateKeyString = new String(data, 0, length);
privateKey = RsaUtil.getPrivateKey(privateKeyString.trim());
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}
// 注入算法生成工具
@Bean
public MathGenerator mathGenerator() {
return new MathGenerator(1);
}
/**
* 自定义的customUsernamePasswordAuthenticationFilter
* 需要同步在HttpSecurity里的配置
*/
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() throws Exception {
CustomUsernamePasswordAuthenticationFilter result = new CustomUsernamePasswordAuthenticationFilter();
result.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(loginApi));
result.setAuthenticationManager(this.authenticationManager());
result.setUsernameParameter(usernameParameter);
result.setPasswordParameter(passwordParameter);
result.setAuthenticationSuccessHandler(new LoginSuccessHandler());
result.setAuthenticationFailureHandler(new LoginFailureHandler());
return result;
}
/**
* 跨域设置
*/
private CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// 允许向该服务器提交请求的URI,* 表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// 允许访问的头信息,* 表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,* 表示全部允许
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 自定义登录页
.loginPage("/login.html")
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 不能写:successForwardUrl("/index.html"),会报405
.defaultSuccessUrl("/index.html")
// 登录失败转发到哪个页面
.failureForwardUrl("/login.html?error=true")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
// 开启认证
.and().authorizeRequests()
//设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/*", "/login/**").permitAll()
//需要认证
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint());
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// 添加验证码过滤器
http.addFilterBefore(new CustomCaptchaFilter(cacheManager, mathGenerator(), entryPoint, loginApi), WebAsyncManagerIntegrationFilter.class);
}
}
I added the verification code filter here WebAsyncManagerIntegrationFilter
to the front. It is the first filter, and the verification code verification party is the first one CustomizeAuthenticationEntryPoint
.
Front-end display verification code (login2.vue)
<template>
<div>
<el-row>
<el-col :span="6">
<el-form :model="form" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="用户名" prop="username">
<el-input type="text" v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="form.password"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="captcha">
<el-input type="text" v-model="form.captcha"></el-input>
<el-row>
<el-col :span="12" ><img :src="captchaImage" alt="0"/></el-col>
<el-col :span="12" ><a href="javascript:void(0)" @click="renewCaptcha">看不清,换一张</a></el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script>
import {
loginApi} from '../api'
import {
JSEncrypt} from 'jsencrypt'
export default {
name: 'login2',
data () {
return {
form: {
username: '',
password: '',
pwd: '',
captcha: ''
},
captchaImage: null,
rules: {
password: [
{
required: true, trigger: 'blur', message: '请输入密码'}
],
username: [
{
required: true, trigger: 'blur', message: '请输入用户名'}
],
captcha: [
{
required: true, trigger: 'blur', message: '请输入验证码'}
]
}
}
},
mounted () {
this.getCaptchaPic()
},
methods: {
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
// 加密密码
this.form.pwd = this.encryptedData(process.env.PUBLIC_KEY.toString(), this.form.password)
this.form.password = null
loginApi.login(this.form, success => {
this.$message.success('登录成功')
// 存储token信息,和用户信息
// 跳转指定页面
console.log(success)
}, error => {
// 验证码过期就刷新验证码
if (error.message.indexOf('过期') > 0) {
this.getCaptchaPic()
}
})
} else {
this.$message.info('表单验证失败')
return false
}
})
},
// 加密密码
encryptedData (publicKey, data) {
// 新建JSEncrypt对象
let encryptor = new JSEncrypt()
// 设置公钥
encryptor.setPublicKey(publicKey)
// 加密数据
return encryptor.encrypt(data)
},
getCaptchaPic () {
loginApi.captcha(null, success => {
this.captchaImage = 'data:image/png;base64,' + success.data
})
},
// 验证码刷新
renewCaptcha () {
this.getCaptchaPic()
}
}
}
</script>
<style scoped>
</style>
Put the verification code on the url here, so that the backend can get the verification code directly
export function login (data, success, error) {
http.post('/loginDeal?captcha=' + data.captcha, data, success, error)
}
result
Interface verification code
In this case, you need to verify the verification code when logging in.
rear end
@GetMapping("/login/captcha/valid")
public Boolean validCaptcha(HttpServletRequest request) {
return authService.validCaptcha(request);
}
The same here, delete the verification code after the verification is successful;
@Override
public boolean validCaptcha(HttpServletRequest request) {
String id = request.getSession().getId();
String key = AuthConstant.buildCaptchaKey(id);
Object match = cacheManager.get(key);
if (match == null) {
throw new RuntimeException("验证码过期!");
}
// 注意这里的验证码参数不是从body里获取的
boolean captcha = mathGenerator.verify(match.toString(), request.getParameter("captcha"));
if (captcha) {
cacheManager.delete(key);
}
return captcha;
}
Frontend (login3.vue)
export function validCaptcha (data, success, error) {
http.get('/login/captcha/valid?captcha=' + data, null, success, error)
}
To log in here, verify the verification code first, and then call the login after success. There are thousands of methods, and I am giving an example here.
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
// 加密密码
this.form.pwd = this.encryptedData(process.env.PUBLIC_KEY.toString(), this.form.password)
this.form.password = null
this.validCaptchaAndLogin(this.form.captcha)
} else {
this.$message.info('表单验证失败')
return false
}
})
},
validCaptchaAndLogin (captcha) {
loginApi.validCaptcha(captcha, success => {
loginApi.login(this.form, success => {
this.$message.success('登录成功')
// 存储token信息,和用户信息
// 跳转指定页面
console.log(success)
}, error => {
// 验证码过期就刷新验证码
if (error.message.indexOf('过期') > 0) {
this.getCaptchaPic()
}
})
}, error => {
console.log(error)
this.getCaptchaPic()
})
}
Authorization/Access Control (custom-auth-control)
Response serialization for permissions
Similarly CustomizeAuthenticationEntryPoint
, configuration serialization is required
/**
* 访问拒绝处理器
* @author ALI
* @since 2023/6/11
*/
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private AuthenticationEntryPoint entryPoint;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ResponseData<Object> result = ResponseData.failed("无权限访问");
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(result));
writer.flush();
}
}
SecurityWebConfig
Change setting
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler());
Configuration of permissions
When saving the role information in our table to security, we need to do some processing. The following is the constructor for customizing user information. Here, a prefix is added to the code of the user role. This is the default method of the security framework ROLE_
.
public CustomUser(SysUser sysUser) {
this.userId = sysUser.getId();
this.username = sysUser.getUsername();
this.password = sysUser.getPassword();
if (!CollectionUtils.isEmpty(sysUser.getRoles())) {
this.authorities = sysUser.getRoles().stream().map(d -> new SimpleGrantedAuthority("ROLE_" + d)).collect(Collectors.toList());
}
}
Enable annotation interception
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityAuthConfig {
}
path authority
WebSecurityConfigurerAdapter
This is accomplished through inheritance void configure(HttpSecurity http) throws Exception
;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler());
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests()
// 路径/test /login多级的都可以放行
.antMatchers("/test/*", "/login/**").permitAll()
// 访问getData2 需要角色dev
.antMatchers("/getData2").hasRole("dev")
// 访问getData3 需要角色 admin
.antMatchers("/getData3").hasRole("admin")
// 访问getData4 需要角色dev test1
.antMatchers("/getData4").hasAnyRole("test1","dev")
// 访问getData5 需要角色test1
.antMatchers("/getData5").hasAuthority("ROLE_test1")
// 访问getData6 需要为127.0.0.1
.antMatchers("/getData6").hasIpAddress("127.0.0.1")
// 访问getData7 需要角色test2 test3
.antMatchers("/getData7").hasAnyAuthority("ROLE_test2", "ROLE_test3")
.anyRequest().authenticated();
}
In security, the role will be prefixed by default , so the prefix should be omitted ROLE_
when using it , and it will be added when judging, but it will not be added when it is included .hasRole,hasAnyRole
ROLE_
Authority
ROLE_
method permissions
Control permissions through annotations, support annotations on classes, and have the latest annotations at the same time.
5 permission annotations provided by spring:
- @PreAuthorize
- @PostAuthorize
- @PreFilter
- @PostFilter
- @Secured
JSR-250 protocol annotations:
- @RolesAllowed
- @PermitAll
- @DenyAll
These annotations need to be able to use@EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
prePostEnabled = true
: Open @PreAuthorize,@PostAuthorize,@PreFilter ,@PostFilter
4 annotations
securedEnabled = true
: open @Secured
annotation
jsr250Enabled = true
: open @RolesAllowed,@PermitAll,@DenyAll
annotation
spring permission annotation
Annotations need to be enabled@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize
Generally, this annotation is used, and this annotation is relatively simple, and true
has access rights when the value is;
// 这种方式匹配的是ROLE_dev,内部会默认添加ROLE_
@PreAuthorize("hasRole('dev')")
@GetMapping("/method1")
public String method1() {
return "method1";
}
// 匹配ROLE_dev
@PreAuthorize("hasAuthority('ROLE_dev')")
@GetMapping("/method2")
public String method2() {
return "method2";
}
// 只能由用户名带后缀 _ad 的访问
@PreAuthorize("principal.username.endsWith('_ad')")
@GetMapping("/method4")
public String method4() {
return "method4";
}
// 只能由用户名和参数相等
@PreAuthorize("principal.username.equals(#name)")
@GetMapping("/method44")
public String method44(String name) {
return "method44";
}
// 新增的用户,用户名只能是 _ad 结尾
@PreAuthorize("#user.username.endsWith('_ad')")
@PostMapping("/method5")
public String method5(@RequestBody SysUser user) {
return "method5";
}
// 是否是 admin 的角色,无关大小写
// principal 为内置对象
@PreAuthorize("principal?.isAdmin()")
@GetMapping("/method7")
public String method7() {
return "method7";
}
In addition there is a role inheritance
@Bean
static RoleHierarchy roleHierarchy() {
return new RoleHierarchyImpl("ROLE_admin > ROLE_dev");
}
// 配置了 ROLE_admin > ROLE_dev
// dev 继承了admin 的权限,使用admin的用户访问
@PreAuthorize("hasRole('dev')")
@GetMapping("/method6")
public String method6() {
return "method6";
}
@PostAuthorize
The function of this annotation is to check the permission after the method is executed, and when the value true
is the access permission;
// 返回的用户id必须 =10
// principal 为内置对象
@PostAuthorize("returnObject.id.equals('10')")
@GetMapping("/method8")
public SysUser method8() {
SysUser result = new SysUser();
result.setId("10");
return result;
}
@PreFilter
@PreFilter
The parameters of the collection type can be filtered, and false
elements are removed when the value is set.
// 过滤出 id= 1 的元素
// filterObject 内置对象,表示集合中的每一个元素
// userList 集合对象
@PreFilter(value = "filterObject.id.equals('1')", filterTarget = "userList")
@PostMapping("/method9")
public List<SysUser> method9(@RequestBody List<SysUser> userList) {
return userList;
}
@PostFilter
@PostFilter
The response of the collection type can be filtered to false
remove elements when the value is .
// 过滤出 id != 1 的元素
@PostFilter(value = "!filterObject.id.equals('1')")
@PostMapping("/method10")
public List<SysUser> method10(@RequestBody List<SysUser> userList) {
return userList;
}
@Secured
This annotation is specially used to determine whether the user has the role; the value is the role name, remember to add the prefixROLE_
// 只允许角色为 ROLE_admin 的用户访问
@Secured("ROLE_admin")
@PostMapping("/method11")
public List<SysUser> method11(@RequestBody List<SysUser> userList) {
return userList;
}
JSR-250 annotations
@RolesAllowe
The value is an array of role names, remember to addROLE_
// 允许角色为 ROLE_admin ROLE_dev 的用户访问
@RolesAllowed({
"ROLE_admin", "ROLE_dev"})
@PostMapping("/method12")
public List<SysUser> method12(@RequestBody List<SysUser> userList) {
return userList;
}
@PermitAll
Direct release, no permission verification, can be @RolesAllowed
mixed with, and cannot be used with spring-defined permission annotations;
@RolesAllowed("ROLE_test1")
@RestController
@RequestMapping("/method2")
public class MethodController2 {
@PermitAll
@GetMapping("/get1")
public String method1() {
return "get1";
}
}
@DenyAll
Conversely @PermitAll
, no one can access
@DenyAll
@GetMapping("/get2")
public String method2() {
return "get2";
}
dynamic permissions
principle:
The implementation of permissions can be refined to url, that is, menu permissions. Of course, button-level permissions also belong to menu permissions, and menu permissions are mounted on roles. Therefore, these menu permissions can be obtained through the logged-in user information. Then access Permission control can be realized in the interceptor.
Option One
Create role and permission query list
@Data
public class RolePermission {
private Integer roleId;
private String roleName;
private String roleCode;
private Integer permissionId;
private Integer parentPermissionId;
private String permissionCode;
private String permissionName;
private String permissionUrl;
}
@Select("select a.id as role_id,a.name as role_name,a.code as role_code,b.name as permission_name,b.code as permission_code,b.id as permission_id , b.parent_id as parent_permission_id,b.url as permission_url "
+ "from sys_role a inner join sys_permission b on a.id = b.role_id where a.deleted = 0")
List<RolePermission> roleList();
After starting the project, cache roles and roles for easy access;
If this is done, the cache needs to be updated when modifying role permissions;
@Service
@AllArgsConstructor
public class RoleServiceImpl implements RoleService , InitializingBean {
private CacheManager cacheManager;
private RoleRepository roleRepository;
@Override
public void afterPropertiesSet() throws Exception {
List<RolePermission> roleList = roleRepository.roleList();
if (roleList.isEmpty()) {
return;
}
Map<String, List<RolePermission>> permissionMap = roleList.stream().collect(Collectors.groupingBy(RolePermission::getRoleCode));
for (Map.Entry<String, List<RolePermission>> entry : permissionMap.entrySet()) {
String key = AuthConstant.ROLE_PRE + entry.getKey();
List<String> collect = entry.getValue().stream().map(RolePermission::getPermissionUrl)
.filter(StringUtils::isNoneBlank).collect(Collectors.toList());
String value = JSON.toJSONString(collect);
cacheManager.set(key, value);
}
}
}
Create a single filter PermissionFilter
to judge whether the request has permission;
public class PermissionFilter extends OncePerRequestFilter {
private final CacheManager cacheManager;
public PermissionFilter(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, FilterChain filterChain)
throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getPrincipal() == null) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if (authentication.getPrincipal() instanceof String) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// 获取认证信息
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.isEmpty()) {
throw new AccessDeniedException("无权限!");
}
// 获取权限信息
Set<String> permissions = new HashSet<>();
for (GrantedAuthority authority : authorities) {
Object o = cacheManager.get(authority.getAuthority());
if (o != null) {
List<String> collect = JSONArray.parseArray(o.toString()).stream().map(Object::toString).collect(Collectors.toList());
permissions.addAll(collect);
}
}
if (permissions.isEmpty()) {
throw new AccessDeniedException("无权限!");
}
// 判断是否有权限访问
String api = servletRequest.getRequestURI();
if (!permissions.contains(api)) {
throw new AccessDeniedException("无权限!");
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
Then add the permission filter to FilterSecurityInterceptor
the front, ExceptionTranslationFilter
behind;
http.addFilterAfter(new PermissionFilter(cacheManager), FilterSecurityInterceptor.class);
Option II
custom-auth-control2
In this solution, modify the implementation of the decision manager inside the security, try not to move the security stuff;
first customize the access decision manager
/**
* 自定义访问决策管理器
* @author ALI
* @since 2023/6/16
*/
public class CustomAccessDecisionManager implements AccessDecisionManager {
private final CacheManager cacheManager;
public CustomAccessDecisionManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
// 获取认证信息
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.isEmpty()) {
throw new AccessDeniedException("无权限!");
}
// 获取权限信息
Set<String> permissions = new HashSet<>();
for (GrantedAuthority authority : authorities) {
Object o = cacheManager.get(authority.getAuthority());
if (o != null) {
List<String> collect = JSONArray.parseArray(o.toString()).stream().map(Object::toString).collect(Collectors.toList());
permissions.addAll(collect);
}
}
if (permissions.isEmpty()) {
throw new AccessDeniedException("无权限!");
}
FilterInvocation filterInvocation = (FilterInvocation) object;
// 判断是否有权限访问
String api = filterInvocation.getRequestUrl();
if (!permissions.contains(api)) {
throw new AccessDeniedException("无权限!");
}
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
Modify the security configurationSecurityWebConfig
@Override
public void init(WebSecurity web) throws Exception {
HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
securityInterceptor.setAccessDecisionManager(new CustomAccessDecisionManager(cacheManager));
web.securityInterceptor(securityInterceptor);
});
}
There is another way to set up here: Spring’s official recommended configuration method ( Java Configuration :: Spring Security ), which provides us with the ability to directly modify internal object properties. Compared with the above method, this can modify almost all objects, including :config,filter,handler,interceptor,provider,strategy,point,voter
http.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
fsi.setPublishAuthorizationSuccess(true);
fsi.setAccessDecisionManager(new CustomAccessDecisionManager(cacheManager));
return fsi;
}
});
Complete configuration:
@Configuration
@EnableWebSecurity
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
private static final String loginApi = "/loginDeal";
private static final String usernameParameter = "username";
private static final String passwordParameter = "pwd";
public static PrivateKey privateKey;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomSecurityContextRepository securityContextRepository;
@Autowired
private CacheManager cacheManager;
public SecurityWebConfig() throws Exception {
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("privateKey.pem")) {
if (is == null) {
throw new RuntimeException("没有读取的密钥!!!");
}
byte[] data = new byte[2048];
int length = is.read(data);
String privateKeyString = new String(data, 0, length);
privateKey = RsaUtil.getPrivateKey(privateKeyString.trim());
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}
@Bean
public MathGenerator mathGenerator() {
return new MathGenerator(1);
}
/**
* 自定义的customUsernamePasswordAuthenticationFilter
* 需要同步在HttpSecurity里的配置
*/
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() throws Exception {
CustomUsernamePasswordAuthenticationFilter result = new CustomUsernamePasswordAuthenticationFilter();
result.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(loginApi));
result.setAuthenticationManager(this.authenticationManager());
result.setUsernameParameter(usernameParameter);
result.setPasswordParameter(passwordParameter);
result.setAuthenticationSuccessHandler(new LoginSuccessHandler());
result.setAuthenticationFailureHandler(new LoginFailureHandler());
return result;
}
/**
* 跨域设置
*/
private CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// 允许向该服务器提交请求的URI,* 表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// 允许访问的头信息,* 表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,* 表示全部允许
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("ali")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.userDetailsService(userDetailsService);
}
@Override
public void init(WebSecurity web) throws Exception {
HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
// 因为FilterSecurityInterceptor是在其他配置完成后执行的,所以只能在这里修改
// 详细看 org.springframework.security.config.annotation.web.builders.WebSecurity#performBuild
// 配置 CustomAccessDecisionManager 方式一
FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
securityInterceptor.setAccessDecisionManager(new CustomAccessDecisionManager(cacheManager));
web.securityInterceptor(securityInterceptor);
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单
// 注意:使用了`BasicAuthenticationFilter`那么在配置`WebSecurityConfigurerAdapter`时,就不要设置`http.httpBasic()`,不然不会走我们自定义的Filter
http
.formLogin()
// 登录的请求接口,对应表单的action,这里只是修改了处理的api,实际处理还是security
.loginProcessingUrl("/loginDeal")
// 登录的用户名和密码参数名称
.usernameParameter("username")
.passwordParameter("pwd")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll()
.and().csrf().disable(); //关闭csrf防护
// 将我们的repository设置到共享变量里
http.setSharedObject(SecurityContextRepository.class, securityContextRepository);
// 设置序列化
http.exceptionHandling().authenticationEntryPoint(new CustomizeAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler());
http.addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests()
.antMatchers("/test/*", "/login/**").permitAll()
.antMatchers("/getData2").hasRole("dev")
.antMatchers("/getData3").hasRole("admin")
.antMatchers("/getData4").hasAnyRole("test1")
.antMatchers("/getData5").hasAuthority("ROLE_test1")
.antMatchers("/getData6").hasIpAddress("127.0.0.1")
.antMatchers("/getData7").hasAnyAuthority("ROLE_test2", "ROLE_test3")
.anyRequest().authenticated()
// 配置 CustomAccessDecisionManager 方式二
// .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
// public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
// fsi.setPublishAuthorizationSuccess(true);
// fsi.setAccessDecisionManager(new CustomAccessDecisionManager(cacheManager));
// return fsi;
// }
// })
;
}
}
Warehouse Address
https://gitee.com/LIRUIYI/test-security.git
https://gitee.com/LIRUIYI/test-security-web.git