一、自定义身份认证和授权流程:
1、身份认证
关于登陆:因sercurity默认登陆接口为 /login,而具体的认证逻辑走完已经实现了登陆(获取了令牌),所以系统中无需再定义登陆controller接口。
从前面的demo中可以看到,当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。如果需要自定义逻辑时,只需要实现 UserDetailsService接口。这样当用户登录是先进入认证过滤器获得用户的账号和密码然后到userDetailService实现类查询用户信息和用户权限之后返回security对象就是userDetail。但是我们需要收集用户名和密码传到UserDetailsService服务中,可以通过继承UsernamePasswordAuthenticationFilter 实现表单参数收集的功能,sercurity默认该过滤器只拦截post请求的 /login 接口。
具体认证流程为:
1.1、获取表单提交参数:
主要是收集用户名、密码。通过继承UsernamePasswordAuthenticationFilter,在attemptAuthentication方法中完成参数收集,收集后调用authenticationManager的authenticate方法完成认证。
1.2、身份认证逻辑
在authenticationManager中,而authenticationManager内部实现调用了UserDetailsService的loadUserByUsername。这一步无需代码实现。
1.3、认证服务
UserDetailsService.loadUserByUsername,返回UserDetails。通过实现UserDetailsService接口来覆写loadUserByUsername方法实现。
1.4、认证成功
UsernamePasswordAuthenticationFilter.successfulAuthentication接收UserDetails,生成自定义token并返回;同时可以将token缓存到redis中,用于后面权限拦截判断token是否过期的安全校验、以及退出功能的实现(删除redis的用户登陆key,这样权限拦截会判断token失效,实现账号退出的功能)。
2、授权
在SpringSecurity中,会使用默认的FilterSecurityInterceptor
来进行权限校验,
在FilterSecurityInterceptor
中会从SecurityContextHolder
获取其中的Authentication
,然后获取其中的权限信息,判断当前用户是否拥有访问当前资源所需的权限。
UsernamePasswordAuthenticationFilter身份认证成功后,其他非 /login接口是否是合法请求?
可以通过继承BasicAuthenticationFilter,在doFilterInternal中做处理:
2.1、判断用户是否合法
从请求中获取token,判断token是否为空、是否过期、是否正确(和token的生成方式一致),校验不通过认为用户不合法,return 返回错误信息。
2.2、合法请求
(1)根据token获取用户id,再根据用户id从数据库中查询资源、角色等(系统常用的)信息,放到SecurityContextHolder全局变量中。
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(currentUser,null,currentUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
后续系统中需要获取当前登陆用户的信息,也直接从 SecurityContextHolder中获取
(UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
当然仅限于配置了该过滤器的接口,对于WebSecurityConfigurerAdapter中配置的白名单接口未设置用户信息自然也取不到。
(2)最后放行,chain.doFilter(req, res);
二、身份认证和授权demo:
下面以一个demo来演示下:
1、pom和application配置文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo.security</groupId>
<artifactId>security-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.14.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
server.port=8089
server.servlet.context-path=/securityDemo
my.redis.server.host= localhost
my.redis.server.port=6379
my.redis.server.jedis.pool.maxTotal=500
my.redis.server.jedis.pool.maxIdle=10
my.redis.server.jedis.pool.maxWaitMillis=5000
my.redis.server.jedis.pool.min-idle=5
my.redis.server.timeout=5000
2、sercurity总配置类
import com.security.demo.filter.AuthenticationFilter;
import com.security.demo.filter.LoginFilter;
import com.security.demo.util.RedisClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisClient redisClient;
/**
* 需要放行的URL
*/
private static final String[] AUTH_WHITELIST = {
"/static/**",
"/index.html",
"/test/test"
};
/**
* 设置 HTTP 验证规则
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin().and().authorizeRequests().anyRequest().authenticated().and().csrf().disable()// 所有请求需要身份认证
.exceptionHandling()
.and()
//1、登陆、退出url,均由前端拦截器控制,这里注释掉。
//1.1、前端拦截器中判断缓存token为空,为空则post请求访问/login,目的是进入LoginFilter获取token
//1.2、不为空则带token访问接口,如果AuthenticationFilter拦截token不合法则根据错误码跳转到登陆页面,重复1.1的操作
//.logout().logoutUrl("/logout").and()
//2、身份认证filter,访问系统(除了白名单接口)需要先登陆。post请求/login接口会进入这个拦截器
// 校验用户名密码是否正确,正确返回token给前端,不正确则返回异常信息
.addFilterBefore(new LoginFilter(authenticationManager(),redisClient),LoginFilter.class)
//3、授权filer,authenticationManager为BasicAuthenticationFilter的必传参数。所有的接口都会走到这里
// 根据用户id查询权限,连同身份一起塞入SecurityContextHolder全局变量,后面获取用户信息则直接从SecurityContextHolder中get
.addFilterBefore(new AuthenticationFilter(authenticationManager(),redisClient,userDetailsService),AuthenticationFilter.class);
}
/**
* 身份认证
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new MyPasswordEncoder());
}
/**
* 配置哪些请求不拦截
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
}
import org.springframework.security.crypto.password.PasswordEncoder;
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
/*public DefaultPasswordEncoder() {
this(-1);
}
*//**
* @param strength
* the log rounds to use, between 4 and 31
*//*
public DefaultPasswordEncoder(int strength) {
}
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}*/
}
3、自定义用户服务UserDetailsService
import com.security.demo.constant.UserConstants;
import com.security.demo.dto.UserDTO;
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;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
/**
* 当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
* 而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑,
*/
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//模拟数据库查询
Map<String, String> userMap = UserConstants.getUsers();
String dbPwd = userMap.get(username);
if(StringUtils.isEmpty(dbPwd)){
throw new UsernameNotFoundException("用户不存在");
}
Map<String, List<String>> userRoles = UserConstants.getUserRoles();
List<String> roles = userRoles.get(username);
Map<String, List<String>> userMenus = UserConstants.getUserPermissions();
List<String> menus = userMenus.get(username);
UserDTO userDTO = new UserDTO(null,null,username,roles,menus,dbPwd);
return userDTO;
}
}
实体:
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class UserDTO implements UserDetails {
private Integer id;
private String userName;
private String userAccount;
private List<String> roles;
private List<String> menus;
private String passWord;
public UserDTO (Integer id,String userName,String userAccount,List<String> roles,List<String> menus,String passWord){
this.id = id;
this.userAccount = userAccount;
this.userName = userName;
this.roles = roles;
this.menus = menus;
this.passWord = passWord;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return passWord;
}
@Override
public String getUsername() {
return this.userAccount;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public List<String> getRoles() {
return roles;
}
public List<String> getMenus() {
return menus;
}
}
4、拦截器
(1)登陆拦截器LoginFilter
import com.security.demo.constant.UserConstants;
import com.security.demo.dto.UserDTO;
import com.security.demo.util.JwtUtil;
import com.security.demo.util.RedisClient;
import org.apache.http.entity.ContentType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
/**
* 登陆拦截器,身份认证
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RedisClient redisClient;
public LoginFilter(AuthenticationManager authenticationManager,RedisClient redisClient) {
this.authenticationManager = authenticationManager;
this.redisClient = redisClient;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
String userName = req.getParameter("userName");
String passWord = req.getParameter("passWord");
if (StringUtils.isEmpty(userName)) {
throw new UsernameNotFoundException("请输入账号");
}
//验证用户名密码是否正确
Map<String, String> userMap = UserConstants.getUsers();
if(!userMap.keySet().contains(userName)){
throw new UsernameNotFoundException("用户不存在");
}
if(!passWord.equals(userMap.get(userName))){
throw new UsernameNotFoundException("密码错误");
}
//这里权限返回空,由后面的授权过滤器查询
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(userName, passWord, new ArrayList<>()));
} catch (UsernameNotFoundException e) {
//返回错误信息
res.setCharacterEncoding("UTF-8");
res.setContentType("application/text;charset=utf-8");
try {
res.getWriter().write(e.getMessage());
} catch (IOException e1) {
e1.printStackTrace();
}
return null;
}catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* 登录成功(即UserDetailsService正常返回)调用的方法,
* 这里返回token给前端
* @param req
* @param res
* @param chain
* @param auth
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
UserDTO userDTO = (UserDTO) auth.getPrincipal();
String userName = userDTO.getUsername();
String password = userDTO.getPassword();
String jwtToken = JwtUtil.sign(userName, password);
//缓存到redis中
redisClient.set(userName,jwtToken);
//返回
res.setContentType(ContentType.TEXT_HTML.toString());
res.getWriter().write(jwtToken);
}
}
(2)token拦截器AuthenticationFilter
import com.security.demo.util.JwtUtil;
import com.security.demo.util.RedisClient;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登陆成功后的filter
*从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
* 如果校验通过,就认为这是一个取得授权的合法请求
*/
public class AuthenticationFilter extends BasicAuthenticationFilter {
private RedisClient redisClient;
private UserDetailsService userDetailsService;
public AuthenticationFilter(AuthenticationManager authenticationManager,RedisClient redisClient,UserDetailsService userDetailsService) {
super(authenticationManager);
this.redisClient = redisClient;
this.userDetailsService = userDetailsService;
}
//非登陆接口,只携带token
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
logger.info("登陆成功后访问,url={}"+req.getRequestURI());
String token = req.getHeader("token");
res.setCharacterEncoding("UTF-8");
res.setContentType("application/text;charset=utf-8");
if(StringUtils.isEmpty(token)){
logger.info("登陆成功后访问,url={},token为空"+req.getRequestURI());
res.getWriter().write("token为空");
return;
}
//1、token是否正确,是否能反解析出userName
String userName = JwtUtil.getUsername(token);
if(StringUtils.isEmpty(userName)){
logger.info("登陆成功后访问,url={},token错误"+req.getRequestURI());
res.getWriter().write("token错误");
return;
}
//2、验证token是否过期
String redisToken = redisClient.get(userName);
if(StringUtils.isEmpty(redisToken) || !token.equals(redisToken)){
logger.info("登陆成功后访问,url={},token过期"+req.getRequestURI());
res.getWriter().write("token过期");
return;
}
UserDetails currentUser = userDetailsService.loadUserByUsername(userName);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(currentUser,null,currentUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
}
5、业务逻辑
(1)常量(模拟数据库查询)
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 模拟数据库查询数据,假设有:用户名/密码/角色/资源
* admin/123/xtgly/user_manage、role_manage、menu_manage、school_manage
* zs/123/userAdmin、roleAdmin/user_manage、role_manage、menu_manage
* ls/123/schoolAdmin/school_manage
*/
public class UserConstants {
//redis缓存过期时间
public static final int EXPIRE_TIME = 30 * 60 * 1000;
public static Map<String, String> getUsers() {
Map<String,String> users = new HashMap<>();
users.put("admin","123");
users.put("zs","123");
users.put("ls","123");
return users;
}
public static Map<String,List<String>> getUserRoles() {
Map<String,List<String>> userRoles = new HashMap<>();
//admin
List<String> adminRoles = new ArrayList<>();
adminRoles.add("xtgly");
userRoles.put("admin",adminRoles);
//zs
List<String> zsRoles = new ArrayList<>();
zsRoles.add("userAdmin");
zsRoles.add("roleAdmin");
userRoles.put("zs",zsRoles);
//ls
List<String> lsRoles = new ArrayList<>();
lsRoles.add("schoolAdmin");
userRoles.put("ls",lsRoles);
return userRoles;
}
public static Map<String,List<String>> getUserPermissions() {
Map<String,List<String>> userPermissions = new HashMap<>();
List<String> lsPermissions = new ArrayList<>();
//ls
lsPermissions.add("school_manage");
userPermissions.put("ls",lsPermissions);
//zs
List<String> zsPermissions = new ArrayList<>();
zsPermissions.add("user_manage");
zsPermissions.add("role_manage");
zsPermissions.add("menu_manage");
userPermissions.put("zs",zsPermissions);
//admin
List<String> adminPermissions = new ArrayList<>();
adminPermissions.add("school_manage");
userPermissions.put("admin",adminPermissions);
return userPermissions;
}
}
(2)controller接口
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private RedisClient redisClient;
@RequestMapping("/test")
public String test(){
return "这是user test";
}
@RequestMapping("/getCurrentUser")
public UserDTO getCurrentUser() {
UserDTO currentUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println("当前用户为:"+currentUser);
return currentUser;
}
//退出登陆
@RequestMapping("/logOut")
public void logOut(HttpServletRequest request){
UserDTO currentUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String userAccount = currentUser.getUsername();
redisClient.del(userAccount);
}
}
@RequestMapping("/school")
@RestController
public class SchoolManageController {
@RequestMapping("/test")
public String test(){
return "这是学校管理";
}
}