springboot+springsecurity+JWT

目录结构:
在这里插入图片描述
springbootApplication里面的配置,如有需要可以自行添加
在这里插入图片描述
实现JWT功能

在pom.xml中的配置

<?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>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
    </parent>

    <groupId>com.my</groupId>
    <artifactId>my-springsecurity</artifactId>
    <version>1.0-SNAPSHOT</version>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- oracle数据库驱动 -->
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>11.2.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>5.0.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>5.0.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.30</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- &lt;!&ndash; 资源文件拷贝插件 &ndash;&gt;
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-resources-plugin</artifactId>
                 <configuration>
                     <encoding>UTF-8</encoding>
                 </configuration>
             </plugin>-->
            <!-- java编译插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

在application.yml中的配置

server:
  port: ${PORT:8080}
spring:
  application:
    name: my-springsecurity
  datasource:
    url: jdbc:oracle:thin:@localhost:1521:orcl
    username: scott
    password: oracle
    driver-class-name: oracle.jdbc.driver.OracleDriver
  redis:
    host: ${REDIS_HOST:127.0.0.1}
    port: ${REDIS_PORT:6379}
    timeout: 5000 #连接超时 毫秒
#  main:
#    allow-bean-definition-overriding: true #同名bean覆盖

auth:
  redisAge: 600 #单位秒
# 无法扫描到resources下的templates中的静态文件时可以配置,如果可以无需配置
#注入到TomcatConfig
bw:
  factory:
    doc:
      root: E:\JAVAIDEA\My\springboot\my-springsecurity\src\main\resources\templates

首先需要继承WebSecurityConfigurerAdapter类,配置security中的认证授权相关配置

SecurityConfig 类

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //token 过滤器,解析token
    @Autowired
    MyJwtTokenFilter jwtTokenFilter;

    
    @Autowired
    MyUserDetailsService myUserDetailsService;

    @Autowired
    private SendSmsSecurityConfig sendSmsSecurityConfig;

    //加密机制
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }


    //认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
//                .antMatchers("/admin/api/**").hasRole("ADMIN")
//                .antMatchers("/user/api/**").hasRole("USER")
                .antMatchers("/SendSms").permitAll()
                .antMatchers("/userlogout").permitAll()
                .antMatchers("/**").permitAll()
                .anyRequest().authenticated()//任何请求登录后访问
                .and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//                .sessionManagement() //会话管理
//                .maximumSessions(1); //限制登录人数
                .and().csrf().disable();

        //登录操作
        http.formLogin()
                .loginPage("/login.html")//登录路径
                .loginProcessingUrl("/farmerlogin")//登录表单提交请求
                .usernameParameter("username")//设置登录账号参数,默认username
                .passwordParameter("password")//设置登录密码参数,默认password
//                .defaultSuccessUrl("/home.html")//登录成功跳转,不能和successHandler一起使用
//                .failureUrl("/login.html")// 登录失败跳转,不能和failureHandler一起使用
                .permitAll()
                //登录成功处理
                .successHandler(new LoginSuccessHandler())
                //登录失败处理
                .failureHandler(new LoginFailureHandler())
                .and().apply(sendSmsSecurityConfig);


        //退出操作
        http.logout()
                .logoutUrl("/aaa")//退出提交参数
                .logoutSuccessUrl("/login.html");//退出后跳转,不能和logoutSuccessHandler一起使用
//        .logoutSuccessHandler();//退出处理

        // 禁用缓存
        http.headers().cacheControl();

        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
                // 添加权限不足 filter
                .exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler())
                //其他异常处理类
                .authenticationEntryPoint(new MyAuthenticationException());
    }

    //忽略拦截
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/elementuidemo/**",
                "/img/**", "/js/**", "/plugins/**", "/static/json/**", "/pages/**");
    }
    
}

配置相关配件:

LoginSuccessHandler 类(登陆成功处理)

登录成功处理:继承SavedRequestAwareAuthenticationSuccessHandler类,该类继承AuthenticationSuccessHandler类,AuthenticationSuccessHandler登录成功父类,之所以用SavedRequestAwareAuthenticationSuccessHandler类,里面可以实现其他的方法,如:登录成功跳转到登录之前请求的页面

/**
 * <p>
 * AuthenticationSuccessHandler登录成功父类
 * SavedRequestAwareAuthenticationSuccessHandler 继承AuthenticationSuccessHandler
 * <p>
 * 登录成功处理
 */
@Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Value("${auth.redisAge}")
    int redisAge;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("登录成功");
        //从authentication中获取用户信息
        final User farmerDetail = (User) authentication.getPrincipal();
        //生成jwt
        String token = jwtTokenUtil.generateToken(farmerDetail);
        httpServletResponse.addHeader("token", "Bearer " + token);
        //把token保存到redis中,farmerDetail.getUsername()用户名作为key,也可以用id作为key值farmerDetail.getId()
        stringRedisTemplate.opsForValue().set(farmerDetail.getUsername(),token,redisAge, TimeUnit.SECONDS);
        httpServletResponse.setContentType("application/json");
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.getWriter().write("{\"result\":\"ok\"}");

    }
}

LoginFailureHandler 类(登录失败处理)

登录失败处理:继承SimpleUrlAuthenticationFailureHandler类,该类继承SimpleUrlAuthenticationFailureHandler类,SimpleUrlAuthenticationFailureHandler类登录失败父类,之所以用SimpleUrlAuthenticationFailureHandler类,里面可以实现其他方法,如:登录失败跳转到登录页面

/**
 *
 * AuthenticationFailureHandler登录失败父类
 * SimpleUrlAuthenticationFailureHandler 继承AuthenticationFailureHandler
 *
 * 登录失败处理
 */
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {

        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.setStatus(401);
        PrintWriter out = httpServletResponse.getWriter();
        out.write("{\"error_code\":\"401\",\"name\":\""+e.getClass()+"\",\"message\":\""+e.getMessage()+"\"}");

    }
}

MyAccessDeniedHandler 类(权限不足处理)

权限不足处理:实现的AccessDeniedHandler类,进行的权限校验

/**
 * Spring security权限不足处理类
 * 只有登录后(即接口有传token)接口权限不足才会进入AccessDeniedHandler,
 * 如果是未登陆或者会话超时等,不会触发AccessDeniedHandler,而是会直接跳转到登陆页面。
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        //登陆状态下,权限不足执行该方法
        System.out.println("权限不足:" + e.getMessage());
        response.setStatus(200);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        response.getWriter().write("{\"result\":\"权限不足\"}");
        printWriter.flush();
    }
}

MyAuthenticationException 类(异常处理)

异常处理:实现AuthenticationEntryPoint类,实现异常的处理,如果不配置此类,则spring security默认会跳转到登录页面

/**
 * Spring security其他异常处理类,比如请求路径不存在等,
 * 如果不配置此类,则Spring security默认会跳转到登录页面
 */
@Component
public class MyAuthenticationException implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        System.out.println("AuthenticationEntryPoint检测到异常:"+e);
        httpServletResponse.setStatus(200);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = httpServletResponse.getWriter();
        httpServletResponse.getWriter().write("AuthenticationEntryPoint检测到异常:"+e);
        printWriter.flush();
    }
}

MyUserDetailsService 类(从数据库查询用户信息)

UserDetailsService验证用户名、密码和授权处理,实现从数据库查询用户功能(仅用于测试,用户和权限在一张表中,根据需求可以进行多表查询赋予权限)

/**
 * 根据账号查询用户
 */
@Component
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;



    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //从数据库读取该用户
        User user = userMapper.findByUserName(username);
        // 用户不存在,抛出异常
        if (user == null){
            throw new UsernameNotFoundException("用户不存在");
        }
        //将数据库形式的roles解析为UserDtails的权限集
//        farmer.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(farmer.getRoles()));
        List<GrantedAuthority> authorities = generateAuthorities(user.getRoles());
        user.setAuthorities(authorities);
        return user;

        //基于内存验证用户信息
//        UserDetails userDetails = Farmer.withUsername("123").password("123").roles("USER").build();
//        return userDetails;
    }

    //自定义实现权限转换
    private List<GrantedAuthority> generateAuthorities(String roles){
        List<GrantedAuthority> authorities = new ArrayList<>();
        String[] roleArray = roles.split(",");
        if (roles != null && !"".equals(roles)){
            for (String role : roleArray) {
                authorities.add(new SimpleGrantedAuthority(role));
            }
        }
        return authorities;
    }
}

MyJwtTokenFilter 类(token过滤器)

token过滤器进行解析判断是否登录,继承OncePerRequestFilter类

/**
 * token 过滤器,在这里解析token,拿到该用户角色,设置到springsecurity的上下文环境中,让springsecurity自动判断权限
 * 所有请求最先进入此过滤器,包括登录接口,而且在springsecurity的密码验证之前执行
 */
@Component
public class MyJwtTokenFilter extends OncePerRequestFilter {

    @Autowired
    MyUserDetailsService myUserDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");
        String tokenHead = "Bearer ";

        if (authHeader != null && authHeader.startsWith(tokenHead)) {
            String token = authHeader.substring(tokenHead.length());
            String username = jwtTokenUtil.getUsernameFromToken(token);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.myUserDetailsService.loadUserByUsername(username);
                //从redis中取出存储的token
                String redisToken = stringRedisTemplate.opsForValue().get(userDetails.getUsername());
                //验证令牌是否有效
                if (token.equals(redisToken) && jwtTokenUtil.validateToken(token, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

JwtTokenUtil 类(JWT工具类)

JWT工具类

@Component
public class JwtTokenUtil implements Serializable {
    /**
     * 密钥
     */
    private final String secret = "aaaa";
    @Value("${auth.redisAge}")
    private Long redisAge;


    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + redisAge * 1000);
    }
    /**
     * 从数据声明生成令牌
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成令牌
     *
     * @param userDetails 用户
     * @return 令牌
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        return generateToken(claims);
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }
}

User 类(实现UserDetails类)

在pojo文件中,写User实体类实现UserDetails类

public class User implements UserDetails{

    private int id;
    private String username;
    private String password;
    private String roles;


    private List<GrantedAuthority> authorities;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }
    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public String getRoles() {
        return roles;
    }

    public void setRoles(String roles) {
        this.roles = roles;
    }

    public void setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }


    //账号是否没过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    //账号是否没被锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    //密码是否没过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    //账号是否可用
    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", roles='" + roles + '\'' +
                ", authorities=" + authorities +
                '}';
    }
}

dao层,mapper.xml,数据库可以进行简单的配置进行测试。数据库为Oracle,可以自行配置mysql或Oracle
在这里插入图片描述
在这里插入图片描述在这里插入图片描述数据库就四个字段,id,username,password,权限。

controller中有三个测试类

@RestController
@RequestMapping("/admin/api")
public class AdminController {

    @GetMapping("hello")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String hello(){
        return "hello,admin";
    }
}
@RestController
@RequestMapping("/app/api")
public class AppController {

    @GetMapping("hello")
    public String hello(){
        return "hello,app";
    }
}
@RestController
@RequestMapping("/user/api")
public class UserController {

    @GetMapping("/hello")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public String hello(){
        return "hello,user";
    }
}

在Postman中进行登录测试

登录成功显示: farmerlogin是在SecurityConfig中自定义的,默认为login
在这里插入图片描述
会在Headers中生成一个token
在这里插入图片描述
这个token是自己定义的,在登录成功处理类中生成的token令牌
在这里插入图片描述
在验证时,在Key中存放 Authorization ,在Value中把生成的token保存进去,然后会在MyJwtTokenFilter类中进行令牌校验
成功示例:
在这里插入图片描述

退出登录时

/**
 * 退出操作
 */
@RestController
@RequestMapping("/")
public class LogoutController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 退出
     * @param username 账号,存入redis中的key值
     */
    @RequestMapping("/userlogout")
    public void logout(@RequestParam("username") String username) {
    	//删除redis中的key
        stringRedisTemplate.delete(username);
    }
}

SendSmsSecurityConfig类为短信登录的配置类,须在SecurityConfig进行声明
SendSms文件中的类为短信登录相关的配置,和本章并无冲突,遇到相关配置可直接删除

test测试文件里面是发送短信测试

TomcatConfig类不重要,需要了可以配置:

TomcatConfig类的配置说明 https://blog.csdn.net/weixin_45498999/article/details/105973351

代码资源:https://download.csdn.net/download/weixin_45498999/12500298

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值