【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
{
@Autowired
private AccessTokenFilter accessTokenFilter;
@Override
protected 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);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(); // 这个会对用户提交的密码进行编码,然后才跟密码库里面的密码进行比较
}
@Bean
@Override
public 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","/"})
@ResponseBody
public 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
{
@Autowired
private AuthenticationManager authenticationManager;
@GetMapping(value = "/hello")
@ResponseBody
public 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")
@ResponseBody
public 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")
@ResponseBody
public 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));
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public 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
{
@Override
protected 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
{
@Override
public 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】
可以看到访问成功了。