Spring Security使用 - Go语言中文社区

Spring Security使用


Spring Security

在web应用开发中,安全无疑是十分重要的,选择Spring Security来保护web应用是一个非常好的选择。

Spring Security 是spring项目之中的一个安全模块,可以非常方便与spring项目无缝集成。特别是在spring boot项目中加入spring security更是十分简单。本篇我们介绍spring security,以及spring security在web应用中的使用。

一个例子入门

假设我们现在创建好了一个springboot的web应用,有一个控制器如下:

@Controller
public class AppController {
 @RequestMapping("/hello")
 @ResponseBody
 String home() {
 return "Hello ,spring security!";
 }
}

我们启动应用,假设端口是8080,那么当我们在浏览器访问http://localhost:8080/hello的时候可以在浏览器看到Hello ,spring security!。

加入spring security 保护应用

此时,/hello是可以自由访问。假设,我们需要具有某个角色的用户才能访问的时候,我们可以引入spring security来进行保护。加入如下maven依赖,并重启应用:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
</dependency>

再次访问/hello,我们可以得到一个http-basic的认证弹窗,如下:

 

代码如下:

<html><head><title>Login Page</title></head><body οnlοad='document.f.username.focus();'>
<h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
<table>
 <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
 <tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
 <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
 <input name="_csrf" type="hidden" value="635780a5-6853-4fcd-ba14-77db85dbd8bd" />
</table>
</form></body></html>

我们可以发现,这里有个form 。action=”/login”,这个/login是spring security提供的。form表单提交了三个数据:

username 用户名   password 密码   _csrf CSRF保护方面的内容

说明spring security 已经起作用了。这时,它为你生成了账号和密码,在内存中。

但是,我们不可能只是这么使用它,我们如何通过访问数据库来登录,验证权限呢?

接下来通过一个实例继续深入。

demo项目权限介绍

我们通过一个很简单的项目来认识一下Spring Security。

index.html:社区首页(只有四个链接)----------任何人都可以访问

discuss.html:帖子详情页面(只有一句话)------任何人都可以访问

letter.html:私信列表(只有一句话)-------只有登陆后的用户才能访问

admin.html:管理员页面(只有一句话)----只有管理员才能访问

login.html:登陆页面(有表单)----------------不符合要求时,可以登录

我们的目的,就是把这些权限管理起来。

 

静态页面代码展示

 

下面展示一下这些页面的代码:

index.html:社区首页(只有四个链接)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>社区首页</h1>
    <ul>
        <li><a th:href="@{/discuss}">帖子详情</a></li>
        <li><a th:href="@{/letter}">私信列表</a></li>
        <li><a th:href="@{/loginpage}">登录</a></li>
        <li><a th:href="@{/loginpage}">退出</a></li>
    </ul>
</body>
</html>

discuss.html:帖子详情页面(只有一句话)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>帖子</title>
</head>
<body>
    <h1>帖子详情页面</h1>
</body>
</html>

letter.html:私信列表(只有一句话)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>私信</title>
</head>
<body>
    <h1>私信列表页面</h1>
</body>
</html>

admin.html:管理员页面(只有一句话)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>管理员</title>
</head>
<body>
    <h1>管理员专属页面</h1>
</body>
</html>

login.html:登陆页面(有表单)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>

    <h1>登录社区</h1>

    <form method="post" action="#">
        <p style="color:red;">
            <!--提示信息-->
        </p>
        <p>
            账号:<input type="text" >
        </p>
        <p>
            密码:<input type="password" >
        </p>
        <p>
            验证码:<input type="text" >
        </p>
        <p>
            <input type="submit" value="登录">
        </p>
    </form>

</body>
</html>

service层和user的操作

首先要处理我们的用户user表,写出获取权限的方法。

实现UserDetails 接口,和接口定义的方法。

方法的作用我都已经标注在代码里。

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

public class User implements UserDetails {

    private int id;
    private String username;
    private String password;
    private String salt;
    private String email;
    private int type;
    private int status;
    private String activationCode;
    private String headerUrl;
    private Date createTime;
/*
get/set方法
toSring方法
*/
    // true: 账号未过期.
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // true: 账号未锁定.
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // true: 凭证未过期.
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // true: 账号可用.
    @Override
    public boolean isEnabled() {
        return true;
    }

    // 返回用户权限
    //我们有两种用户:1代表管理员,2代表普通用户
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> list = new ArrayList<>();
        list.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                switch (type) {
                    case 1:
                        return "ADMIN";
                    default:
                        return "USER";
                }
            }
        });
        return list;
    }

}

着重介绍一下getAuthorities方法:

它的返回值是一个权限集合,因为我们真实开发可能是这样的:

用户表:记录了用户类型(是普通用户还是1级管理员、2级管理员、卖家买家等等)

权限表:记录了每个角色有什么权限,比如普通用户可以发帖评论点赞,管理员可以删贴置顶等等。

最后我们通过用户类型,查到多种权限,并且返回。

因为每种用户有多种权限,所以getAuthorities方法的返回值是一个权限集合。,这个集合可以装很多GrantedAuthority对象。

本代码的集合只装了一个权限对象,并且重写了对应回去权限的方法。。


service层实现接口和对应方法。

import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    public User findUserByName(String username) {
        return userMapper.selectByName(username);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return this.findUserByName(username);
    }
}

这个接口是要实现查找对应的用户。

我们自己写登录逻辑的时候,一样要这么做:用账号(id)查到用户,读取密码,看看用户输入的和在数据库查到的是否相同,

security底层也是做了类似的事情,所以我们需要告诉他如何查找用户。

如果我们之前写过selectByName之类的方法,可以直接调用即可。

这里我把dao层和xml实现也给出来。

import com.nowcoder.community.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    User selectByName(String username);
}
<mapper namespace="com.community.dao.UserMapper">

    <sql id="selectFields">
        id, username, password, salt, email, type, status, activation_code, header_url, create_time
    </sql>

    <select id="selectByName" resultType="User">
        select
        <include refid="selectFields"></include>
        from user
        where username = #{username}
    </select>

</mapper>

核心操作

书写配置类统一管理。

有详细的注释。

通常我们需要书写如下代码:

忽略哪些资源?

认证

授权

package com.community.config;

import com.community.entity.User;
import com.community.service.UserService;
import com.community.util.CommunityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 忽略静态资源的访问
        web.ignoring().antMatchers("/resources/**");
    }

    // AuthenticationManager: 认证的核心接口.
    // AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具.
    // ProviderManager: AuthenticationManager接口的默认实现类.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 内置的认证规则
        // auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));

        // 自定义认证规则
        // AuthenticationProvider: ProviderManager持有一组AuthenticationProvider,每个AuthenticationProvider负责一种认证.
        // 委托模式: ProviderManager将认证委托给AuthenticationProvider.
        auth.authenticationProvider(new AuthenticationProvider() {
            // Authentication: 用于封装认证信息的接口,不同的实现类代表不同类型的认证信息.
            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                String username = authentication.getName();
                String password = (String) authentication.getCredentials();

                User user = userService.findUserByName(username);
                if (user == null) {
                    throw new UsernameNotFoundException("账号不存在!");
                }

                password = CommunityUtil.md5(password + user.getSalt());
                if (!user.getPassword().equals(password)) {
                    throw new BadCredentialsException("密码不正确!");
                }

                // principal: 主要信息; credentials: 证书; authorities: 权限;
                return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
            }

            // 当前的AuthenticationProvider支持哪种类型的认证.
            @Override
            public boolean supports(Class<?> aClass) {
                // UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类.
                return UsernamePasswordAuthenticationToken.class.equals(aClass);
            }
        });
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 登录相关配置
        http.formLogin()
                .loginPage("/loginpage")
                .loginProcessingUrl("/login")
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.sendRedirect(request.getContextPath() + "/index");
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                        request.setAttribute("error", e.getMessage());
                        request.getRequestDispatcher("/loginpage").forward(request, response);
                    }
                });

        // 退出相关配置
        http.logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.sendRedirect(request.getContextPath() + "/index");
                    }
                });

        // 授权配置
        http.authorizeRequests()
                .antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")
                .antMatchers("/admin").hasAnyAuthority("ADMIN")
                .and().exceptionHandling().accessDeniedPage("/denied");

        // 增加Filter,处理验证码
        http.addFilterBefore(new Filter() {
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest request = (HttpServletRequest) servletRequest;
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                if (request.getServletPath().equals("/login")) {
                    String verifyCode = request.getParameter("verifyCode");
                    if (verifyCode == null || !verifyCode.equalsIgnoreCase("1234")) {
                        request.setAttribute("error", "验证码错误!");
                        request.getRequestDispatcher("/loginpage").forward(request, response);
                        return;
                    }
                }
                // 让请求继续向下执行.
                filterChain.doFilter(request, response);
            }
        }, UsernamePasswordAuthenticationFilter.class);

        // 记住我
        http.rememberMe()
                .tokenRepository(new InMemoryTokenRepositoryImpl())
                .tokenValiditySeconds(3600 * 24)
                .userDetailsService(userService);

    }
}

表单

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>

    <h1>登录社区</h1>

    <form method="post" th:action="@{/login}">
        <p style="color:red;" th:text="${error}">
            <!--提示信息-->
        </p>
        <p>
            账号:<input type="text" name="username" th:value="${param.username}">
        </p>
        <p>
            密码:<input type="password" name="password" th:value="${param.password}">
        </p>
        <p>
            验证码:<input type="text" name="verifyCode"> <i>1234</i>
        </p>
        <p>
            <input type="checkbox" name="remember-me"> 记住我
        </p>
        <p>
            <input type="submit" value="登录">
        </p>
    </form>

</body>
</html>

HomeController 

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.User;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HomeController {

    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String getIndexPage(Model model) {
        // 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.
        Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (obj instanceof User) {
            model.addAttribute("loginUser", obj);
        }
        return "/index";
    }

    @RequestMapping(path = "/discuss", method = RequestMethod.GET)
    public String getDiscussPage() {
        return "/site/discuss";
    }

    @RequestMapping(path = "/letter", method = RequestMethod.GET)
    public String getLetterPage() {
        return "/site/letter";
    }

    @RequestMapping(path = "/admin", method = RequestMethod.GET)
    public String getAdminPage() {
        return "/site/admin";
    }

    @RequestMapping(path = "/loginpage", method = {RequestMethod.GET, RequestMethod.POST})
    public String getLoginPage() {
        return "/site/login";
    }

    // 拒绝访问时的提示页面
    @RequestMapping(path = "/denied", method = RequestMethod.GET)
    public String getDeniedPage() {
        return "/error/404";
    }

}

 

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/hebtu666/article/details/103313824
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-25 02:25:31
  • 阅读 ( 1166 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢