springSecurity集成springboot
将springSecurity集成springboot,前后端分离的方式开发,使用token验证,实现单点登录,在权限控制方面,使用注解的方式进行控制
引入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
代码实现:
- UserDetailsService接口重写登录逻辑
- 登录成功拦截器的重写
- 登录失败拦截器的重写
- 认证逻辑过滤器的重写
- 认证权限不足的拦截
- 未登录,进行访问权限的拦截
- 登出成功的拦截
java代码配置类:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Resource
private AuthenticationFailureHandler authenticationFailureHandler;
@Resource
private AuthenticationEntryPoint authenticationEntryPoint;
@Resource
private AccessDeniedHandler myAccessDeniedHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new MD5Util());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.usernameParameter("account")
.and().csrf().disable();
http.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new MyLogoutSuccessHandler());
http.addFilter(new MyBasicAuthenticationFilter(authenticationManager()))
.httpBasic();
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler);
// 管理员才可以查看swagger文档
// http.authorizeRequests()
// .antMatchers("/swagger-ui.html").hasAnyRole("ROLE_admin");
// http.authorizeRequests()
// .antMatchers("/user/register").permitAll()
// .anyRequest().authenticated(); // 所有请求都拦截,使用注解的方式权限控制时,不使用这个,
}
}
UserDetailsService接口重写登录逻辑:
// MyUserDetailsService implements UserDetailsService
@Service
public class MyUserDetailsServiceImpl implements MyUserDetailsService {
@Resource
private UserService userService;
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
if (!StringUtils.hasText(account)){
throw new RuntimeException("用户名不能为空!!!");
}
User user = (User) userService.getUserByName(account).getData();
if (user == null){
throw new RuntimeException("此账号还未注册");
}
List<GrantedAuthority> grantedAuthorityList =
AuthorityUtils.commaSeparatedStringToAuthorityList(user.getAdmin() == 0 ? "ROLE_admin" : "ROLE_normal");
return new org.springframework.security.core.userdetails.User(user.getAccount(),user.getPassword(),grantedAuthorityList);
}
}
登录成功拦截器的重写:
@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String account = String.valueOf(request.getParameter("account"));
User user = (User) userService.findUserByAccount(account).getData();
// 得到权限
String permissions = user.getAdmin() == 0 ? "ROLE_normal" : "ROLE_admin" ;
String token = JwtUtil.createToken(user.getId(),user.getAccount(),permissions);
// token 发给前端
response.setHeader("Authorization",token);
response.setHeader("Access-Control-Expose-Headers", "Authorization");
log.info("用户: [{}] 登录成功!!! , 权限为: [{}]",account , permissions);
// SecurityContextHolder 赋值,用于授权
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(user.getAccount(),token,
AuthorityUtils.commaSeparatedStringToAuthorityList(permissions))
);
ResponseUtil.out(response,Result.success(token));
}
}
登录失败拦截器的重写:
@Component
@Slf4j
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.warn("用户: [{}] 尝试登录,登录失败!!!",httpServletRequest.getParameter("account"),e);
// 此为手动抛出异常
if (e.getCause()!=null){
ResponseUtil.out(httpServletResponse,Result.fail(555,e.getCause().getMessage()));
}
ResponseUtil.out(httpServletResponse, Result.fail(HttpCode.PASSWORD_ERROR));
}
}
认证逻辑过滤器的重写:
/**
* 重写授权的逻辑
*/
@Slf4j
public class MyBasicAuthenticationFilter extends BasicAuthenticationFilter {
public MyBasicAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader("Authorization");
// 没有token,视为未登录
if (!StringUtils.hasText(token)) {
chain.doFilter(request,response);
return;
}
Claims claims = JwtUtil.decryptToken(token);
if (claims == null){
ResponseUtil.out(response, Result.fail(HttpCode.TOKEN_ERROR));
return;
}
Long id = Long.valueOf(claims.getId());
String account = claims.getSubject();
String permissions = (String) claims.get("permissions");
log.info("正在授权中-----id为: [{}] , 账号为: [{}] , 具有的权限: {}" ,id,account,permissions);
// 认证成功,给SecurityContextHolder填充数据,用于验证授权
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(account,token,
AuthorityUtils.commaSeparatedStringToAuthorityList(permissions))
);
chain.doFilter(request,response);
}
}
认证权限不足的拦截:
/**
* 登录的用户,权限不足的拦截
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
ResponseUtil.out(httpServletResponse, Result.fail(HttpCode.NO_PERMISSIONS));
}
}
未登录,进行访问权限的拦截:
/**
* 未登录的用户,进行拦截
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(httpServletResponse, Result.fail(HttpCode.NO_LOGIN));
}
}
登出成功的拦截:
@Slf4j
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
if (authentication != null){
new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);
}
ResponseUtil.out(httpServletResponse, Result.success());
}
}
工具类
// SecurityContextHolder 类中存的是认证用户的信息
public class SecurityContextUtil {
/**
* 通过SpringSecurity的上下文获取用户的token,得到id
* @return
*/
public static Long getCurrentUserId(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) return null;
String token = (String) authentication.getCredentials();
if (!StringUtils.hasText(token)){
return null;
}
Claims claims = JwtUtil.decryptToken(token);
if (claims == null){
return null;
}
return Long.valueOf(claims.getId());
}
}
/**
* 自定义加密方式,盐值改变会导致一些用户无法登录
*/
public class MD5Util implements PasswordEncoder {
// 盐值
private static String salt = PropertiesConfig.getSalt();
/**
* 加密
* 加密两次,第二次加密放上盐值
* @param charSequence 字符串
* @return
*/
@Override
public String encode(CharSequence charSequence) {
String data = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
return DigestUtils.md5DigestAsHex((data+salt).getBytes());
}
public static String encode(String charSequence){
String data = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
return DigestUtils.md5DigestAsHex((data+salt).getBytes());
}
public static String encode(String charSequence,String salt){
String data = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
return DigestUtils.md5DigestAsHex((data+salt).getBytes());
}
/**
* 匹配是否相等
* @param charSequence 未加密的字符串
* @param s 已加密的字符串
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
if (!StringUtils.hasText(charSequence)) return false;
String data = encode(charSequence);
return data.equals(s);
}
public static boolean matches(String charSequence,String salt, String encrypt) {
if (!StringUtils.hasText(charSequence)) return false;
String data = DigestUtils.md5DigestAsHex(charSequence.getBytes());
data = DigestUtils.md5DigestAsHex((data+salt).getBytes());
return data.equals(encrypt);
}
}
// token工具
@Slf4j
public class JwtUtil {
private static String key = PropertiesConfig.getKey();
private static Integer time = PropertiesConfig.getTime();
public static String createToken(Map<String, String> map) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, time);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, key);
map.forEach((key, value) -> {
jwtBuilder.claim(key, value);
});
// 设置 签名时间, 过期时间
String token = jwtBuilder.setIssuedAt(new Date())
.setExpiration(calendar.getTime()).compact();
return token;
}
public static String createToken(Long id, String account, String permissions) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, time);
log.info("有效时间: {}",time);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, key);
jwtBuilder.setId(String.valueOf(id));
jwtBuilder.setSubject(account);
jwtBuilder.claim("permissions",permissions);
// 设置 签名时间, 过期时间
String token = jwtBuilder.setIssuedAt(new Date())
.setExpiration(calendar.getTime()).compact();
return token;
}
public static Claims decryptToken(String token) {
try {
Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
return claims;
} catch (Exception e) {
log.info("解析token失败");
return null; // 解密失败则返回null
}
}
}
使用:
@RestController
@RequestMapping("/category")
public class CategoryController {
@Resource
private CategoryService categoryService;
@GetMapping("/all")
@PreAuthorize("permitAll()")
public Result all(){
return categoryService.all();
}
@PostMapping("/add")
@PreAuthorize("hasAnyRole('admin')")
public Result add(@RequestBody @Validated CategoryAddParam categoryAddParam){
return categoryService.add(categoryAddParam);
}
@PostMapping("/update")
@PreAuthorize("hasAnyRole('admin')")
public Result update(@RequestBody @Validated CategoryUpdateParam categoryUpdateParam){
return categoryService.update(categoryUpdateParam);
}
@PostMapping("/delete")
@PreAuthorize("hasAnyRole('admin')")
public Result delete(@RequestBody Map<String,Long> map){
Long id = map.get("id");
if (id == null){
return Result.fail(HttpCode.PARAM_ERROR);
}
return categoryService.delete(id);
}
/**
* 根据名称查找类别
*/
@PostMapping("/findCategoryByName")
@PreAuthorize("permitAll()")
public Result findCategoryByName(@RequestBody Map<String,String> map){
String categoryName = map.get("categoryName");
return categoryService.findCategoryByName(categoryName);
}
@PostMapping("fuzzy")
@PreAuthorize("permitAll()")
public Result fuzzy(@RequestBody CategoryFuzzyParam categoryFuzzyParam){
return categoryService.fuzzy(categoryFuzzyParam);
}
@PostMapping("hot")
@PreAuthorize("permitAll()")
public Result hot(@RequestBody(required = false) Map<String,Integer> map){
if (map == null) map = new HashMap<>();
Integer limit = map.get("limit");
return categoryService.hot(limit);
}
/**
* 上传图片
*/
@PostMapping("/uploadAvatar")
@PreAuthorize("hasAnyRole('admin')")
public Result uploadAvatar(@RequestParam("image")MultipartFile file) throws IOException {
String url = FileUploadUtil.upload(PropertiesConfig.getUploadCategoryDir(), file, FileUploadUtil.IMAGE_ALLOWED_EXTENSION);
return Result.success(FileUploadUtil.transformUrl(url));
}
}
致此集成springSecurity完毕,若我的代码有不足之处,还请大家斧正。