Spring Security can use @PreAuthorize and @PostAuthorize for access control, as described below.
The project uses Springboot2.3.3+SpringSecurtiy+Mbatis to achieve.
First introduce pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 需要单独添加thymeleaf的布局模块 -->
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
The log4j2 is used in the project to replace the logback log system, and the configuration file of log4j2 needs to be configured.
The first step: realize the configuration of SpringSecurity
Extend the configuration class of WebSecurityConfigurerAdapter
/**
* @author MaLei
* @description: 新建一个WebSecurityConfig类,使其继 承WebSecurityConfigurerAdapter
* 在给WebSecutiryConfig类中加上@EnableWebSecurity 注解后,便会自动被 Spring发现并注册(查看
* @EnableWebSecurity 即可看到@Configuration 注解已经存在
* @create 2020/7/14
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启全局方法配置这个注解必须开启否则@PreAuthorize等注解不生效
public class WebSecutiryConfig extends WebSecurityConfigurerAdapter {
//认证管理器配置方法可以配置定定义的UserDetailService和passwordEncoder。无需配置springboot2.3会自动注入bean
/* @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(UserDetailService)
.passwordEncoder(new BCryptPasswordEncoder());
}*/
//核心过滤器配置方法
//void configure(WebSecurity web)用来配置 WebSecurity。而 WebSecurity是基于 Servlet Filter用来配置 springSecurityFilterChain。而 springSecurityFilterChain又被委托给了 Spring Security 核心过滤器 Bean DelegatingFilterProxy。 相关逻辑你可以在 WebSecurityConfiguration中找到。一般不会过多来自定义 WebSecurity, 使用较多的使其ignoring()方法用来忽略Spring Security对静态资源的控制.对于静态资源的忽略尽量在此处设置,否则容易无限循环重新定向到登录页面
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**", "/mylogin.html","/admin", "/favicon.ico");
}
//安全过滤器链配置方法
//void configure(HttpSecurity http)这个是我们使用最多的,用来配置 HttpSecurity。 HttpSecurity用于构建一个安全过滤器链 SecurityFilterChain。SecurityFilterChain最终被注入核心过滤器 。 HttpSecurity有许多我们需要的配置。我们可以通过它来进行自定义安全访问策略
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http); 不能使用默认的验证方式
//authorizeRequests()方法实际上返回了一个 URL 拦截注册器,我们可以调用它提供的
//anyanyRequest()、antMatchers()和regexMatchers()等方法来匹配系统的URL,并为其指定安全
//策略
http.authorizeRequests()
.anyRequest().authenticated()
.and()
//formLogin()方法和httpBasic()方法都声明了需要Spring Security提供的表单认证方式,分别返
//回对应的配置器
.formLogin()
//,formLogin().loginPage("/myLogin.html")指定自定义的登录
//页/myLogin.html,同时,Spring Security会用/myLogin.html注册一个POST路由,用于接收登录请求
//loginProcessingUrl("/login")指定的/login必须与表单提交中指向的action一致
.loginPage("/mylogin.html").loginProcessingUrl("/logins").permitAll()
//表单中用户名和密码对应参数设置(默认为username和password),如果是默认值则不用设置下面的参数对应.
.usernameParameter("usernames").passwordParameter("passwords")
.successForwardUrl("/hello")
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(403);
String error=new String();
if (e instanceof BadCredentialsException ||
e instanceof UsernameNotFoundException) {
error="账户名或者密码输入错误!";
} else if (e instanceof LockedException) {
error="账户被锁定,请联系管理员!";
} else if (e instanceof CredentialsExpiredException) {
error="密码过期,请联系管理员!";
} else if (e instanceof AccountExpiredException) {
error="账户过期,请联系管理员!";
} else if (e instanceof DisabledException) {
error="账户被禁用,请联系管理员!";
} else {
error="登录失败!";
}
httpServletResponse.getWriter().write("{\"message\":\""+error+"\"}");
}
})
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException {
resp.setContentType("application/json;charset=utf-8");
@Cleanup PrintWriter out = resp.getWriter();
out.write("{\"msg\":\"注销成功!\"}");
out.flush();
// out.close();
}
})
.permitAll()
.and()
//csrf()方法是Spring Security提供的跨站请求伪造防护功能,当我们继承WebSecurityConfigurer
//Adapter时会默认开启csrf()方法
.csrf().disable()
//只有确实的访问失败才会进入AccessDeniedHandler,如果是未登陆或者会话超时等,不会触发AccessDeniedHandler,而是会直接跳转到登陆页面
.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString("{\"message\":\"权限不足,请联系管理员!\"}"));
out.flush();
out.close();
}
});
}
/**
* 增加密码加密器,一旦增加,在验证过程中security将使用密码加密器进行加密对比,数据库中如果存储明文密码,在
* UserDetailsService接口实现方法中,先加密密码然后才能返回UserDetails
* @return
*/
@Bean
PasswordEncoder passwordEncoder(){
//使用系统自带密码加密器也可以参考上一篇自己继承PasswordEncoder接口写编码器
return new BCryptPasswordEncoder();
}
}
In the configuration file, static resources, login pages, etc. can be accessed without a login account. I put them in the public void configure(WebSecurity web) throws Exception
method.
Step 2: Implement UserDetails and UserDetailsService interfaces.
The User class implements the UserDetails interface
public class User implements Serializable, UserDetails {
private Long id;
private String username;
private String password;
private String name;
private Boolean enabled;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
private static final long serialVersionUID = 1L;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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 enabled;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
//将当前账户的所属角色进行配置
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list=new ArrayList<>();
//将当前用户的配属角色填入集合
Assert.notNull(roles,"角色集合为null");
for (Role r:roles){
list.add(new SimpleGrantedAuthority(r.getName()));
}
return list.size()>0?list:null;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", username=").append(username);
sb.append(", password=").append(password);
sb.append(", name=").append(name);
sb.append(", enabled=").append(enabled);
sb.append("]");
return sb.toString();
}
}
The UserDetailService class implements the UserDetailsService interface
/**
* @author MaLei
* @description: UserDetailService
* @create 2020/7/14
*/
@Component
@Slf4j
public class UserDetailService implements UserDetailsService {
@Autowired
UserMapper customerMapper;
@Override
@Transactional("firstTransactionManager")
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("登录账号:{}",username);
//数据库读取账户,如果读出来的密码是明码必须使用PasswordEncoder加密
User cust=customerMapper.selectByUserNameContainsRoles(username);
if(cust==null)
throw new UsernameNotFoundException("账户不存在");
return cust;
}
}
Due to the database operation implemented by Mybatis, the specific mapper implementation will not be posted.
The third step is to use @PreAuthorize and @PostAuthorize on the previous Controller method.
@PreAuthorize is to perform permission authentication
before the method is executed @PostAuthorize is to perform permission authentication before returning after the method is executed
@RestController
public class TestController {
@RequestMapping("/hello")
@PreAuthorize("hasPermission('/hello', 'read') or hasRole('ROLE_admin')")
public String hello() {
return "hello";
}
The code @PreAuthorize("hasPermission('/hello','read') or hasRole('ROLE_admin')") also uses hasRole('ROLE_admin'), which means that the currently logged-in user can access as long as he has the role of ROLE_admin . The role name in hasRole('ROLE_admin') can be written as ROLE_admin or abbreviated as admin. The system will automatically determine whether there is a ROLE_ prefix, and if it is not, it will be automatically added.
The hasPermission('/hello','read') system will not automatically process it, and it needs to implement the PermissionEvaluator interface for processing.
The fourth step: implement the PermissionEvaluator interface
/**
* @author MaLei
* @description: PermissionEvaluator接口实现类
* @create 2020/7/17
*/
@Configuration
public class MyPermissionEvaluator implements PermissionEvaluator {
@Autowired
MenuMapper menuMapper;
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
boolean accessable = false;
if(authentication.getPrincipal().toString().compareToIgnoreCase("anonymousUser") != 0){
Menu menu= menuMapper.selectByRequestUrl(targetDomainObject.toString());
if(menu==null) return accessable;
List<Role> roles = menu.getRoles();
Iterator<Role> it=roles.iterator();
while(it.hasNext()) {
Role role=it.next();
for (GrantedAuthority authority : authentication.getAuthorities()) {
if(role.getName().equals(authority.getAuthority())){
accessable=true;
}
}
}
}
return accessable;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission)
The method is to process
@PreAuthorize("hasPermission('/hello','read') or hasRole('ROLE_admin')")
the information passed in in the hasPermission('/hello','read') annotation
, Authentication in the hasPermission method The authentication parameter represents the logged-in user and permission information
Object targetDomainObject parameter represents the first parameter "/hello" in
hasPermission('/hello','read') Object permission parameter represents the second parameter in hasPermission('/hello','read') Parameter "read"
In the high version of springboot, after implementing the PermissionEvaluator interface, as long as the implementation class is added to the @Configuration system, the system will automatically register to the container.
If in the lower version of springboot, the custom PermissionEvaluator implementation class does not take effect, you need to declare a @Bean in your own implementation of the WebSecutiryConfig configuration class, in the following form:
/**
* 注入自定义PermissionEvaluator
*/
@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler(){
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
//将自己实现的PermissionEvaluator接口实现类加入处理器
handler.setPermissionEvaluator(new MyPermissionEvaluator());
return handler;
}
Through the above configuration, the use of @PreAuthorize, @PostAuthorize and other annotations to restrict permissions is realized. In this example, all requests must be logged in to access. If an account is logged in, but after a certain resource is requested, the resource method is not available. Add the @PreAuthorize or @PostAuthorize annotation, it means that the resource does not need access permission and can be accessed directly. Pay attention to this point.