一、基本业务开发
1.1、需求分析
由于Security对用户进行鉴权和授权是通过用户名去数据库中取权限,所以我们需要开发一个功能,这个功能就是通过username去数据库里查该用户所具备的所有权限
1.2、完成需求
1.2.1、数据库脚本
请下载文章末尾的源代码
1.2.2、UserDAO.xml中的核心代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="demo.dao.UserDao">
<resultMap id="userAnthMap" type="Users">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="account" property="account"></result>
<result column="password" property="password"></result>
<collection property="anths" ofType="java.lang.String">
<result column="anth_code" />
</collection>
</resultMap>
<select id="queryUserInfoAndAnths" resultMap="userAnthMap" >
SELECT
u.id,
u.username,
u.account,
u.password,
ta.anth_code
FROM
t_user u
LEFT JOIN t_user_anth tua ON u.id = tua.user_id
LEFT JOIN t_anth ta ON tua.anth_id = ta.id
where u.account = #{account}
</select>
</mapper>
二、Security底层开发
2.1、鉴权服务类SecurityService
根据username去数据库中查询该用户所具备的权限,并通过Security进行授权
package demo.service;
import demo.dao.UserDao;
import demo.domain.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
* 获取用户名和密码的service
*/
@Service
public class SecurityService implements UserDetailsService {
@Autowired
@Lazy
private PasswordEncoder passwordEncoder;
@Autowired(required=false)
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据username,去数据库查该用户的信息
Users users = userDao.queryUserInfoAndAnths(username);
//设置的authorityString必须和SecurityConfig中设置的hasAuthority字符串一致
//当前用户具有insert权限,具有admin角色(角色必须加ROLE_的前缀)
String anths = String.join(",", users.getAnths());
if (users != null) {
return new User(users.getAccount(),passwordEncoder.encode(users.getPassword()),
AuthorityUtils.commaSeparatedStringToAuthorityList(anths));
} else {
throw new UsernameNotFoundException("用户名和密码输入错误!");
}
}
}
2.2、登录成功后的处理器
package demo.handler;
import com.alibaba.fastjson.JSON;
import demo.util.JWTUtil;
import demo.util.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;
/**
* 前后端分离的项目情况下,登录成功后,返回的不再是一个页面地址,还是一个json
* 处理用户登录成功后返回数据:比如用户名等信息
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication)
throws IOException, ServletException {
/**
* 获取登录成功的用户信息
*/
try {
User user = (User)authentication.getPrincipal();
// 把jwt放入redis,并设置有效时间
String jwt = JWTUtil.createJWT(user.getUsername());
//TimeUnit.SECONDS : 秒
//TimeUnit.MINUTES : 分钟
redisTemplate.opsForValue().set("jwt:"+user.getUsername(), jwt,30, TimeUnit.MINUTES);
// 设置字符集
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter pw = httpServletResponse.getWriter();
String json = JSON.toJSONString(new ResponseResult<>().ok(jwt));
pw.print(json);
pw.flush();
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.3、登录失败后的处理器
package demo.handler;
import com.alibaba.fastjson.JSON;
import demo.util.ResponseResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 登录失败后进去,返回给前端提示信息
*/
public class LoginFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 设置字符集
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter pw = httpServletResponse.getWriter();
String json = JSON.toJSONString(ResponseResult.FAIL);
pw.print(json);
pw.flush();
pw.close();
}
}
2.4、用户未登录直接访问系统资源的处理器
package demo.handler;
import com.alibaba.fastjson.JSON;
import demo.util.ResponseResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 前后后端分离项目情况下:用户未登录直接访问系统资源,
* 会被该类拦截
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 设置字符集
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter pw = httpServletResponse.getWriter();
String json = JSON.toJSONString(ResponseResult.NOLOGIN);
pw.print(json);
pw.flush();
pw.close();
}
}
2.5、拦截没有权限访问该资源的处理器
就算登陆成功了,但是如果该用户没有访问该资源的权限,则进行处理
package demo.handler;
import com.alibaba.fastjson.JSON;
import demo.util.ResponseResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 虽然你知道用户名,和密码
* 但是,拦截你没有权限访问该资源操作
*
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 设置字符集
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter pw = httpServletResponse.getWriter();
String json = JSON.toJSONString(ResponseResult.NOAUTH);
pw.print(json);
pw.flush();
pw.close();
}
}
2.6、用户注销的处理器
在前后端分离项目中,会让session失效,所以该处理器可以不进行配置
package demo.handler;
import com.alibaba.fastjson.JSON;
import demo.util.ResponseResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 用户退出操作
*/
public class MyLogoutSuccesshandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 设置字符集
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter pw = httpServletResponse.getWriter();
String json = JSON.toJSONString(ResponseResult.LOGOUT);
pw.print(json);
pw.flush();
pw.close();
}
}
2.7、注册jwt的处理器
使用redis对jwt进行保存,在该处理器中对合法的jwt的用户进行redis续期
package demo.handler;
import demo.service.SecurityService;
import demo.util.JWTUtil;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
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;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* security 整合jwt用的过滤器
* 功能点1:判断出登录请求外,是否携带了jwt, 否:放掉不处理
* 功能点2:判断携带的jwt 是否合法,否:放掉不处理
* 功能点3:拿redis的jwt和请求头的jwt 做对比
* 3.1:redis的jwt 已经过期 放掉不处理
* 3.2:在jwt的值对比一下 放掉不处理
*
*/
@Component
public class JWTFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Autowired
private SecurityService service;
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain)
throws ServletException, IOException {
//功能点1:在请求头拿到jwt
String jwt = httpServletRequest.getHeader("jwt");
if (jwt == null){
//放给security 其他过滤器,该方法不做处理
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
// 功能点2:jwt不合法
if (!JWTUtil.decode(jwt)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
//功能点3 获取jwt的用户信息
Map payLoad = JWTUtil.getPayload(jwt);
String username = (String) payLoad.get("username");
//拿到redis的jwt
String redisJWT = redisTemplate.opsForValue().get("jwt:" + username);
//判断redis是否有该jwt
if (redisJWT == null){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
if (!jwt.equals(redisJWT)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
//给redis 的jwt续期
redisTemplate.opsForValue().set("jwt:" + username,jwt,30,
TimeUnit.MINUTES);
//获取用户名,密码,权限
UserDetails userDetails = service.loadUserByUsername(username);
// 获取用户信息 生成security容器凭证
UsernamePasswordAuthenticationToken upa =
new UsernamePasswordAuthenticationToken(userDetails.getUsername()
,userDetails.getPassword(),userDetails.getAuthorities());
//放入凭证
SecurityContextHolder.getContext().setAuthentication(upa);
// 本方法共功能执行完了,交给下一个过滤器
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
2.8、在Security配置类中进行总配置
- 配置注解鉴权
- 配置鉴权的UserService
- 配置各种过滤器
- 配置跨域请求
- 配置session禁用
- 配置跨站脚本攻击
package demo.config;
import demo.handler.*;
import demo.service.SecurityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security配置类
*/
@EnableWebSecurity
//开启security注解,用到哪个注解,就将哪一个注解设置为true
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityService securityService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
/**
* 注入spring容器
*/
@Autowired
private JWTFilter jwtFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(securityService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限访问时跳转的自定义页面
http.formLogin() //表单登陆
.loginProcessingUrl("/user/login") //登录访问的路径
.successHandler(loginSuccessHandler) //【登录成功后的处理器】从login.html页面登录成功之后跳转到该路径,用于前后端分离项目返回用户信息
.failureHandler(new LoginFailHandler()) //登录失败后的处理器
.permitAll();
/**
* 未登录,就想访问系统资源
*/
http.exceptionHandling().
authenticationEntryPoint(new MyAuthenticationEntryPoint())
.accessDeniedHandler(new MyAccessDeniedHandler());
/**
* 退出系统
*/
http.logout().logoutSuccessHandler(new MyLogoutSuccesshandler());
http.authorizeHttpRequests() //认证配置
.anyRequest() //任何请求
.authenticated(); //所有请求都拦截
http.cors(); //开启跨域,需结合demo.config.CorsConfig
// 注册jwt的过滤器
http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);
//前后端项目中要禁用掉session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable(); //关闭跨站脚本攻击
}
/**
* 将PasswordEncoder注入到ioc容器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
三、测试
不登录的情况下请求Controller
登陆之后获取jwt
携带jwt请求Controller
四、源代码
Security在前后端分离项目中的综合应用.zip