加入security+jwt安全策略

Pom中引入

        <!-- security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

加入后访问页面会弹出一个登录页,用户名为user,密码为启动时显示的一串字符串

 
image.png

但是这种页面及默认用户显然不是我们想要的,我们需要用户数据持久化,也需要合理分配权限。如下我们开始对security做基本的设置。

关于security+jwt主要有如下几个类设置

1.新建JwtUserDetails实现UserDetails

package com.tangruo.example.common.security;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * 安全用户模型
 * @author 
 */
public class JwtUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    /**
     * 用户名:这是数据库的字段,
     * 是userName或者是account就写对应的字段
     */
    private String username;

    /**
     * 密码
     */
    private String password;
    private String salt;
    /**
     *  权限集合
     */
    private Collection<? extends GrantedAuthority> authorities;

    JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.salt = salt;
        this.authorities = authorities;
    }

    /**无论我数据库里的字段是 `account`,或者username,或者userName,或者其他代表账户的字段,
     * 这里还是要写成 `getUsername()`,因为是继承的接口
     *
     * @return
     */
    @Override
    public String getUsername() {
        return username;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

    public String getSalt() {
        return salt;
    }

    /**
     * 返回给用户的角色列表
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    /**
     * 账户是否未过期
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    /**
     *账户是否未锁定
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    /**
     *密码是否未过期
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    /**
     *账户是否激活
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }

}

2.新建UserDetailsServiceImpl实现UserDetailsService

package com.rexyn.common.security;


import com.rexyn.system.entity.User;
import com.rexyn.system.service.AuthorityService;
import com.rexyn.system.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;


/**
 * 用户登录认证信息查询
 *
 * @author 
 */

3.权限不足返回,新建AuthenticationAccessDeniedHandler实现AccessDeniedHandler类

package com.tangruo.example.common.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tangruo.example.common.api.ResultJson;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.httpclient.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
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;

/**
 * @author
 */
@Component
@Slf4j
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 登陆状态下,权限不足执行该方法
        // log.error("权限不足:" + accessDeniedException.getMessage());
        response.setStatus(HttpStatus.SC_OK);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        //printWriter.println(objectMapper.writeValueAsString(Result.fail(HttpStatus.SC_FORBIDDEN, accessDeniedException.getMessage())));
        printWriter.println(objectMapper.writeValueAsString(ResultJson.fail(HttpStatus.SC_FORBIDDEN+"", "权限不足,无法访问")));
        printWriter.flush();
        printWriter.close();
    }

}

4.登陆失效设置,新建JwtAuthenticationEntryPoint实现AuthenticationEntryPoint

package com.tangruo.example.common.security;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.httpclient.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tangruo.example.common.api.ResultJson;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 
     */
    private static final long serialVersionUID = -4957913354424675827L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        // 验证为未登陆状态会进入此方法,认证错误
//      log.error("认证失败,请求接口:{},请求IP:{},请求参数:{},失败原因:{}", request.getRequestURI(), WebUtil.getIP(request),
//              JsonUtil.toJson(request.getParameterMap()), authException.getMessage());
        response.setStatus(HttpStatus.SC_OK);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        //printWriter.write(objectMapper.writeValueAsString(Result.fail(HttpStatus.SC_FORBIDDEN, authException.getMessage())));
        printWriter.write(objectMapper.writeValueAsString(ResultJson.fail(HttpStatus.SC_FORBIDDEN+"", "登录已失效,请重新登录")));
        printWriter.flush();
        printWriter.close();
    }


}

5.登陆验证规则重写,新建JwtAuthenticationProvider继承DaoAuthenticationProvider

package com.tangruo.example.common.security;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

import com.tangruo.example.common.util.PasswordEncoder;


/**重写身份验证规则
 * @author tangruo
 *
 * 2021年1月4日
 */
public class JwtAuthenticationProvider extends DaoAuthenticationProvider{

    public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
        setUserDetailsService(userDetailsService);
     }

     @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();
        String salt = ((JwtUserDetails) userDetails).getSalt();
        // 覆写密码验证逻辑
        if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
            logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
}

6.新建WebSecurityConfig继承WebSecurityConfigurerAdapter

package com.tangruo.example.common.config;



import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import com.tangruo.example.common.security.AuthenticationAccessDeniedHandler;
import com.tangruo.example.common.security.JwtAuthenticationEntryPoint;
import com.tangruo.example.common.security.JwtAuthenticationProvider;


/**
 * 全局@author Sheldon
 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * Spring会自动寻找实现接口的类注入,会找到我们的 UserDetailsServiceImpl  类
     */
    @Qualifier("userDetailsServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;


    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */


    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义身份验证组件
        auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 进行授权成功后处理。授权成功后,重定向回之前访问的页面(获取RequestCache中存储的地址)
        SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setTargetUrlParameter("redirectTo");
        // 允许页面内嵌显示
        http.headers().frameOptions().disable();
        
        // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
        http.cors().and().csrf().disable()
                // 认证失败处理类
                .exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .and()
                .authorizeRequests()
                // 跨域预检请求
                //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 允许对于网站静态资源的无授权访问
                .antMatchers(HttpMethod.GET,
                        
                        "/favicon.ico",
                        
                        "/**/*.css",
                        "/**/*.js",
                        "/**/*.jpg",
                        "/**/*.png"
                        
                ).permitAll()
                // 静态资源文件夹和swagger
                .antMatchers("/lib/**").permitAll()
                .antMatchers("/images/**").permitAll()
                .antMatchers("/temp/**").permitAll()
                
                .antMatchers("/doc.html").permitAll()
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources").permitAll()
                .antMatchers("/v2/api-docs").permitAll()
                .antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
                .antMatchers("/swagger/user/login").permitAll()
                // web jars
                .antMatchers("/webjars/**").permitAll()
                // 查看SQL监控(druid)
                .antMatchers("/druid/**").anonymous()
                .antMatchers("/home/**").permitAll()
                .antMatchers("/finance/**").permitAll()
                .antMatchers("/equipment/**").permitAll()
                .antMatchers("/equipment/findMainEquipments").permitAll()
                // 验证码
                .antMatchers("/oauth/captcha**").permitAll()
                .antMatchers("/layuiadmin/**").permitAll()
                //.antMatchers("").hasAnyAuthority("'',''")
                // 注册和登陆
                .antMatchers("/pages/system/login.jsp").permitAll()
                .antMatchers("/commons/*.jsp").permitAll()
                .antMatchers("/").permitAll()
                .antMatchers("/user/*").permitAll()
                .antMatchers("/index").permitAll()
                
                //所有页面放开权限访问
                .antMatchers("/pages/**").permitAll()
                //配置生产管理接口访问权限登陆验证
                .antMatchers("/production/**").permitAll()
                
                //其他所有请求需要身份认证
                .anyRequest().authenticated()

                // 无权限的时候调用AuthenticationAccessDeniedHandler
                .and().exceptionHandling()
                .accessDeniedHandler(getAccessDeniedHandler())

        ;
        // 退出登录处理器
        http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
        // token验证过滤器
        http.addFilterBefore(new BasicAuthenticationFilter(authenticationManager()),
                UsernamePasswordAuthenticationFilter.class);
         }

    @Bean
    @Override
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Bean
    public AccessDeniedHandler getAccessDeniedHandler() {
        return new AuthenticationAccessDeniedHandler();
    }
}

7.新建管理登陆接口类

包含自定义登录接口,登出接口,主页跳转功能

package com.tangruo.example.system.controller;

import com.alibaba.fastjson.JSONObject;
import com.tangruo.example.common.api.ResultJson;
import com.tangruo.example.common.security.CookieUtil;
import com.tangruo.example.common.security.JwtAuthenticatioToken;
import com.tangruo.example.common.security.JwtTokenUtils;
import com.tangruo.example.common.security.SecurityUtils;
import com.tangruo.example.common.util.DateUtils;
import com.tangruo.example.common.util.PasswordUtils;
import com.tangruo.example.common.util.StringUtil;
import com.tangruo.example.system.entity.AuthInfo;
import com.tangruo.example.system.entity.Resource;
import com.tangruo.example.system.entity.User;
import com.tangruo.example.system.service.ResourceService;
import com.tangruo.example.system.service.UserService;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
@Api(value = "用户登陆/退出管理", tags = "用户登陆/退出管理")
@Controller
public class UserLandingController {

    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private UserService sysUserService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private ResourceService resourceService;
   
    @ApiOperation(value = "跳转主页,做动态菜单处理")
    @RequestMapping("/index")
    public String index( HttpServletRequest request,HttpServletResponse res) {
        Authentication authentication=SecurityContextHolder.getContext().getAuthentication();
        String username=authentication.getName();
        if (authentication.getName().equals("anonymousUser")||authentication.getPrincipal() instanceof String) {
            return "/system/login";
        }
        List<Resource> resources=resourceService.findByUsername(username);
        logger.info(JSONObject.toJSONString(resources));
        request.setAttribute("resource", resources);
        
        
        return "/index";
    }
    
    @ApiOperation(value = "主页识别")
    @RequestMapping("/")
    public void defaultIndex( HttpServletRequest request,HttpServletResponse res) throws IOException{
        Authentication authentication=SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication.getName().equals("anonymousUser")||authentication.getPrincipal() instanceof String) {
            res.sendRedirect("/pages/system/login.jsp?");
            return;
        }
        
        res.sendRedirect("/index");
    }

    @ApiOperation(value = "用户登陆")
    @PostMapping("/user/login")
    public void sysUserLogin( HttpServletRequest request,HttpServletResponse res) throws IOException{
        
        logger.info("the login is running");
        String username=request.getParameter("username");
        String password=request.getParameter("password");
        if (StringUtil.isBlank(username)||StringUtil.isBlank(password)) {
            String msg="请输入用户名或密码";
            msg=URLEncoder.encode(msg,"UTF-8");
            res.sendRedirect("/pages/system/login.jsp?msg="+msg);
         return;
        }
        User sysUser = sysUserService.findUser(username,null);
        ResultJson<Object> resultJson = this.login(username , password,sysUser);
        if (resultJson != null) {
            String msg=resultJson.getMsg();
            msg=URLEncoder.encode(msg,"UTF-8");
            res.sendRedirect("/pages/system/login.jsp?msg="+msg);
            return;
        }
        
        // 系统登录认证
        AuthInfo authInfo = this.createAuthInfo(request, username , password);
        if (authInfo == null) {
            String msg="凭证错误";
            msg=URLEncoder.encode(msg,"UTF-8");
             res.sendRedirect("/pages/system/login.jsp?msg="+msg);
             return;
        }
        if (!StringUtil.isEmpty(sysUser.getLoginDate())) {
            authInfo.setLastUpdateTime(sysUser.getLoginDate());
        }
        sysUser.setLoginDate(DateUtils.getCurrDate());
        sysUserService.saveOrUpdate(sysUser);
        
        sysUser.setPassword(null);
        sysUser.setName(sysUser.getName()==null?sysUser.getUsername():sysUser.getName());
        HttpSession session = request.getSession();
        session.setAttribute("currentUser", sysUser);
        res.sendRedirect("/index");
    }
    // 校验账号和密码
    public  ResultJson<Object> login(String username, String password,User sysUser) {
        String salt;
        String objectPassword;
        Integer status;

        // 用户信息
        if (sysUser == null) {
            return ResultJson.fail("TX000001" , "账号不存在");
        }
        salt = sysUser.getSalt();
        objectPassword = sysUser.getPassword();
        status = sysUser.getEnable();
        // 账号不存在、密码错误
        if (!PasswordUtils.matches(salt, password, objectPassword)) {
            return ResultJson.fail("TX000002" , "密码不正确");
        }
        // 账号锁定
        if (status == 0) {
            return ResultJson.fail("TX000003" , "账号已被锁定,请联系管理员");
        }
        return null;
    }
    /**
     * 生成AuthInfo
     *
     */
    public AuthInfo createAuthInfo(HttpServletRequest request, String userName, String password) {
        try {
            JwtAuthenticatioToken token = SecurityUtils.login(request, userName, password, authenticationManager);
            logger.info("the token is: " + token);
            CookieUtil.writeCookie("token" , token.getToken());
            CookieUtil.writeCookie("uid" , userName);
            AuthInfo authInfo = new AuthInfo();
            authInfo.setTokenType(SecurityUtils.BEARER);
            authInfo.setUserName(userName);
            authInfo.setToken(token.getToken());
            authInfo.setExpiresIn(JwtTokenUtils.getExpire());
            return authInfo;
        } catch (Exception e) {
            logger.error("生成AuthInfo出错" , e);
            return null;
        }
    }
    @ApiOperation(value = "登出" , notes = "退出登录")
    @GetMapping(value = "/user/logout")
    public void logout(HttpServletRequest request,HttpServletResponse response) throws IOException {
        String token = StringUtil.isEmpty(request.getHeader("token")) ? CookieUtil.getCookie(request, "token") : request.getHeader("token");
         String msg = "退出登录成功";
         msg=URLEncoder.encode(msg,"UTF-8");
         if (token == null) {
            msg="退出登录失败";
             response.sendRedirect("/pages/system/login.jsp?msg="+msg);
             return;
         }
         CookieUtil.removeCookie("token");
         response.sendRedirect("/pages/system/login.jsp?msg="+msg);
    }

}

我用的登陆失效返回都是json数据,如果需要返回登录页,可以对4中进行设置。
有可能在上面的引用中会出现缺少类的问题。我这变引入了很多工具类,项目我重新打了一次压缩包(example-manage-2.0.zip)。可以进项目查看。
下载地址 网盘: https://pan.baidu.com/s/1c44hr0uCfRI_693JII6ylA 提取码: ghbk
返回目录springboot项目创建过程+maven+mybatis-plus+swagger+security
下一章:十.权限的使用




posted @ 2021-05-15 11:50  牧之丨  阅读(441)  评论(0编辑  收藏  举报