【pom.xml】
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.3.12.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId><version>2.3.12.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>2.3.12.RELEASE</version>
</dependency>
【/resources/public/login.html】
<!DOCTYPE html>
<html><head><title>Spring Security Example </title></head><body><form action="/security/login2" method="get"><div><label> User Name : <input type="text" name="username" value="user"/> </label></div><div><label> Password: <input type="password" name="password" value="123"/> </label></div><div><input type="submit" value="Sign In"/></div></form></body>
</html>
【SecurityConfig.java】
package com.chz.mySpringSecurity.config;import com.chz.mySpringSecurity.filter.AccessTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{@Autowiredprivate AccessTokenFilter accessTokenFilter;@Overrideprotected void configure(HttpSecurity http) throws Exception{http.authorizeRequests()//访问"/"和"/home"路径的请求都允许.antMatchers("/", // 这个会跳转到主页,要放过"/home", // 这是主页的地址,要放过"/security/login2" // 这个是提交登录的地址,要放过).permitAll()//而其他的请求都需要认证.anyRequest().authenticated().and()//修改Spring Security默认的登陆界面.formLogin().loginPage("/security/login") // 登录地址是【/security/login】.permitAll().and().logout().permitAll();http.addFilterBefore(accessTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder(); // 这个会对用户提交的密码进行编码,然后才跟密码库里面的密码进行比较}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
【PublicController.java】
package com.chz.mySpringSecurity.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;@Slf4j
@Controller
@RequestMapping("/")
public class PublicController
{@GetMapping(value = {"/home","/"})@ResponseBodypublic String home(){log.info("chz >>> SecurityController.home(): ");return "this is home page!";}}
【SecurityController.java】
package com.chz.mySpringSecurity.controller;import com.chz.mySpringSecurity.entity.LoginUser;
import com.chz.mySpringSecurity.entity.LoginUsers;
import com.chz.mySpringSecurity.entity.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;@Slf4j
@Controller
@RequestMapping("/security")
public class SecurityController
{@Autowiredprivate AuthenticationManager authenticationManager;@GetMapping(value = "/hello")@ResponseBodypublic String hello(){log.info("chz >>> SecurityController.hello(): ");return "this is hello page!";}@GetMapping(value = "/login")public String login(HttpServletRequest request, HttpServletResponse response){log.info("chz >>> SecurityController.login(): ");return "/login.html";}@GetMapping(value = "/login2")@ResponseBodypublic ResponseResult login2(@RequestParam String username, @RequestParam String password){log.info("chz >>> SecurityController.login2(): username={}, password={}", username, password);UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 这个会触发【UserDetailsService.loadUserByUsername(String username)】方法被调用if(Objects.isNull(authenticate)){log.info("chz >>> SecurityController.login2(): 用户名或密码错误");throw new RuntimeException("用户名或密码错误");}LoginUser loginUser = (LoginUser)authenticate.getPrincipal();loginUser.setRefreshToken(Math.abs(ThreadLocalRandom.current().nextLong())+"");loginUser.setAccessToken(Math.abs(ThreadLocalRandom.current().nextLong())+"");LoginUsers.users.put(loginUser.getAccessToken(), loginUser);log.info("chz >>> accessToken: " + loginUser.getAccessToken());// context里面设置了【authenticationToken】就表示用户已经登录了,但是这个是根据cookie里面有sessionId判断的,跟accessToken无关SecurityContextHolder.getContext().setAuthentication(authenticationToken);return new ResponseResult(200,"登陆成功", loginUser.getAccessToken());}@GetMapping(value = "/logout2")@ResponseBodypublic ResponseResult logout2(@RequestParam String accessToken){log.info("chz >>> SecurityController.login2(): accessToken={}", accessToken);LoginUsers.users.remove(accessToken);// context里面清除了【authenticationToken】就表示用户已经退出登录了,但是这个是根据cookie里面有sessionId判断的,跟accessToken无关SecurityContextHolder.getContext().setAuthentication(null);return new ResponseResult(200,"退出成功");}
}
【LoginUser.java】
package com.chz.mySpringSecurity.entity;import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.HashSet;@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails
{private String accessToken;private String refreshToken;private HashSet<GrantedAuthority> authorities = new HashSet<>();private User user;public LoginUser(User user){this.user = user;}public void addAuthority(String authority){authorities.add(new SimpleGrantedAuthority(authority));}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
【LoginUsers.java】
package com.chz.mySpringSecurity.entity;import java.util.concurrent.ConcurrentHashMap;public class LoginUsers
{// 这个map模拟的是分布式缓存redis的数据,代表已登录的用户列表public static ConcurrentHashMap<String, LoginUser> users = new ConcurrentHashMap<>();
}
【ResponseResult.java】
package com.chz.mySpringSecurity.entity;import lombok.Getter;
import lombok.Setter;@Getter
@Setter
public class ResponseResult<T> {private Integer code;private String msg;private T data;public ResponseResult(Integer code, String msg) {this.code = code;this.msg = msg;}public ResponseResult(Integer code, T data) {this.code = code;this.data = data;}public ResponseResult(Integer code, String msg, T data) {this.code = code;this.msg = msg;this.data = data;}
}
【User.java】
package com.chz.mySpringSecurity.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable
{private String userName;private String password;
}
【MyExceptionHandler.java】
package com.chz.mySpringSecurity.exceptions;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j
@RestControllerAdvice
public class MyExceptionHandler
{@ExceptionHandler(Exception.class)public String handleException(Exception e){log.error("chz >>> err", e);return "发生异常了";}
}
【AccessTokenFilter.java】
package com.chz.mySpringSecurity.filter;import com.chz.mySpringSecurity.entity.LoginUser;
import com.chz.mySpringSecurity.entity.LoginUsers;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Slf4j
@Component
public class AccessTokenFilter extends OncePerRequestFilter
{@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException{log.info("chz >>> ChzAuthenticationTokenFilter.doFilterInternal(): uri:{}, queryParam={}", request.getRequestURI(), request.getQueryString());LoginUser loginUser = null;String accessToken = request.getParameter("accessToken");if( !StringUtils.isEmpty(accessToken) ) {loginUser = StringUtils.isEmpty(accessToken) ? null : LoginUsers.users.get(accessToken);}if( loginUser!=null ){// 这是登录过的,可以访问受限资源UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, loginUser.getPassword(), loginUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);}else {// 没有accessToken清空掉authentication,不让访问受限资源SecurityContextHolder.getContext().setAuthentication(null);}// 不管有没有登录,后面的【UsernamePasswordAuthenticationFilter】会进行权限判断,也直接放过filterChain.doFilter(request, response);}
}
【UserRepository.java】
package com.chz.mySpringSecurity.repository;import com.chz.mySpringSecurity.entity.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.concurrent.ConcurrentHashMap;public class UserRepository
{// 这个用于模拟数据库里面的可登录用户的信息public static ConcurrentHashMap<String, User> users = new ConcurrentHashMap<>();static {//User user = new User();user.setUserName("user");user.setPassword(new BCryptPasswordEncoder().encode("123"));users.put(user.getUserName(), user);//User admin = new User();admin.setUserName("admin");admin.setPassword(new BCryptPasswordEncoder().encode("456"));users.put(admin.getUserName(), admin);}
}
【UserDetailsServiceImpl.java】
package com.chz.mySpringSecurity.service;import com.chz.mySpringSecurity.entity.LoginUser;
import com.chz.mySpringSecurity.entity.User;
import com.chz.mySpringSecurity.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{log.info("chz >>> UserDetailsServiceImpl.loadUserByUsername(): username={}", username);User user = UserRepository.users.get(username);if( user==null ){throw new UsernameNotFoundException("用户名不存在");}LoginUser loginUser = new LoginUser(user);loginUser.addAuthority("chz_role1");return loginUser;}
}
【MySpringSecurityMain.java】
package com.chz.mySpringSecurity;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;@EnableGlobalMethodSecurity(prePostEnabled = true)
@SpringBootApplication
public class MySpringSecurityMain
{public static void main(String[] args){SpringApplication.run(MySpringSecurityMain.class, args);}
}
运行【MySpringSecurityMain】。
访问【http://localhost:8080/security/hello】 ,可以看到被重定向到登录页面【http://localhost:8080/security/login】
用户名输入【user】,密码输入【123】,点击【Sign In】登录。
可以得到一个【accessToken】=【8496842402128172477】
再次访问【http://localhost:8080/security/hello】,可以看到虽然已经登录成功了,但还是被重定向到了登录页面【http://localhost:8080/security/login】。
这是因为【url】里面没有带上【accessToken】,【AccessTokenFilter】自动将用户的登录信息清除了。
带上accessToken再试试试访问【http://localhost:8080/security/hello?accessToken=848234722712551727】
可以看到访问成功了。