很久以前就想搭建一個屬於自己的博客網站。即可以提高自己的編程能力,也可以讓自己的博客生產量提高一些,終於在經歷了myblog1.0、2.0的版本之後,今天“刪庫”,重新開始搭建我的個人blog
一 所使用的技術棧
我是一名後端開發人員(在校大二學生),所以我的技術棧以後端爲主。前端我是找的UI框架,現在的UI框架有很多,改一改就可以用了。以下是我使用的技術棧
工具 | 名稱 |
---|---|
編譯器 | idea |
編程語言 | JAVA1.8 |
數據庫 | mysql 8.0 |
項目框架 | SSM |
權限控制 | spring security |
緩存 | redis |
構建工具 | Maven |
接口調試 | swagger |
二 功能需求分析
1.用戶管理
- 註冊
- 登錄
- 修改密碼
- 增加用戶
- 刪除用戶
- 搜索用戶
2.安全管理
- 角色授權
- 權限設置
3.博客管理
- 發表博客
- 編輯博客
- 刪除博客
- 博客分類
- 設置標籤
- 上傳圖片
- 模糊查詢
- 最新排序
- 最熱排序
- 閱讀量統計
4.評論管理
- 發表評論
- 刪除評論
- 統計評論數
5.點贊管理
- 點贊
- 取消點贊
- 點贊量統計
6.分類管理
- 創建分類
- 編輯分類
- 刪除分類
- 按分類查詢
7.標籤管理
- 創建標籤、
- 刪除標籤
- 按標籤查詢
8.首頁搜索
- 全文檢索
- 最新文章
- 最熱文章(閱讀量 點贊量)
- 熱門標籤
- 熱門用戶
- 熱門文章
三 用戶登錄
數據庫User表
後臺對應User類
這裏我是用的是mybatis自動生成代碼工具 mybatis-generator:generate -e 網上教程很多,用起來也很方便 不做多餘闡述
public class User implements Serializable {
private Long id;
private String phoneNumber;
private String password;
private Integer role;
private String username;
private String trueName;
private String email;
private String birthday;
private String headPortrait;
private static final long serialVersionUID = 1L;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber == null ? null : phoneNumber.trim();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public Integer getRole() {
return role;
}
public void setRole(Integer role) {
this.role = role;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
public String getTrueName() {
return trueName;
}
public void setTrueName(String trueName) {
this.trueName = trueName == null ? null : trueName.trim();
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email == null ? null : email.trim();
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday == null ? null : birthday.trim();
}
public String getHeadPortrait() {
return headPortrait;
}
public void setHeadPortrait(String headPortrait) {
this.headPortrait = headPortrait == null ? null : headPortrait.trim();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", phoneNumber=").append(phoneNumber);
sb.append(", password=").append(password);
sb.append(", role=").append(role);
sb.append(", username=").append(username);
sb.append(", trueName=").append(trueName);
sb.append(", email=").append(email);
sb.append(", birthday=").append(birthday);
sb.append(", headPortrait=").append(headPortrait);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
登錄方式採用JWT的登錄方式 即JSON WEB TOKEN
- 生成token工具類
public class JwtTokenUtil {
public static final long seriaVersionUID = -3301605591108950415L;
static final String CLAIM_KEY_USERNAME = "sub";
static final String CLAIM_KEY_AUDIENCE = "audience";
static final String CLAIM_KEY_CREATED = "created";
private static final String AUDIENCE_UNKNOWN = "unknown";
private static final String AUDIENCE_WEB = "web";
private static final String AUDIENCE_MOBILE = "mobile";
private static final String AUDIENCE_TABLET = "tablet";
//當前的簽名的祕鑰
private String secret = "blog";
//token的有效時間 約25min
private Long expiration = 1296000L;
public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
public Date getCreatDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}
//得到token的有效期
private Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
public String getAudienceFromToken(String token) {
String audience;
try {
final Claims claims = getClaimsFromToken(token);
audience = (String) claims.get(CLAIM_KEY_AUDIENCE);
} catch (Exception e) {
audience = null;
}
return audience;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
//設置過期時間
private Date generateExpeirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private Boolean isCreatedAfterTenMinutes(Date created) {
int minutes = (int) ((System.currentTimeMillis() - created.getTime()) / (1000 * 60));
if (minutes >= 10) {
return true;
}
return false;
}
private String generateAudience(Device device) {
String audience = AUDIENCE_UNKNOWN;
if (device.isNormal()) {
audience = AUDIENCE_WEB;
} else if (device.isTablet()) {
audience = AUDIENCE_TABLET;
} else if (device.isMobile()) {
audience = AUDIENCE_MOBILE;
}
return audience;
}
private Boolean ignoreTokenExpiration(String token) {
String audience = getAudienceFromToken(token);
return (AUDIENCE_TABLET.equals(audience) || AUDIENCE_MOBILE.equals(audience));
}
String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpeirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
//判斷是否在10分鐘後並在有效期內
public Boolean canTokenBeRefreshed(String token) {
final Date created = getCreatDateFromToken(token);
return token != null && created != null && isCreatedAfterTenMinutes(created)
&& (!isTokenExpired(token)) || ignoreTokenExpiration(token);
}
public String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
log.info("獲取要刷新的token: {}", refreshedToken);
return refreshedToken;
}
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getCreatDateFromToken(token);
return (username.equals(user.getUsername())) && !isTokenExpired(token);
}
}
- 配置自己的攔截器
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setHeader("Access_Control_Allow_Origin","*");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html; charset=utf-8");
ResultVO result = ResultVOUtil.error(ResultEnum.AUTHENTICATION_ERROR);
log.info("需要身份認證:{}" ,result);
httpServletResponse.getWriter().append(JSON.toJSONString(result));
}
}
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setHeader("Access_Control_Allow_Origin","*");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html; charset=utf-8");
ResultVO result = ResultVOUtil.error(ResultEnum.AUTHENTICATION_ERROR);
log.info("需要身份認證:{}" ,result);
httpServletResponse.getWriter().append(JSON.toJSONString(result));
}
}
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setHeader("Access_Control_Allow_Origin","*");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html; charset=utf-8");
ResultVO result = ResultVOUtil.error(ResultEnum.AUTHENTICATION_ERROR);
log.info("需要身份認證:{}" ,result);
httpServletResponse.getWriter().append(JSON.toJSONString(result));
}
}
- Security User
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setHeader("Access_Control_Allow_Origin","*");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html; charset=utf-8");
ResultVO result = ResultVOUtil.error(ResultEnum.AUTHENTICATION_ERROR);
log.info("需要身份認證:{}" ,result);
httpServletResponse.getWriter().append(JSON.toJSONString(result));
}
}
@Service
@Slf4j
public class JwtUserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String phoneNum) throws UsernameNotFoundException {
User user = userService.getUserByPhoneNum(phoneNum);
if (user == null) {
log.info("此用戶不存在");
throw new UsernameNotFoundException(String.format("用戶名爲 %s 的用戶不存在", phoneNum));
} else {
String role = RoleEnum.getRole(user.getRole());
return new JwtUser(phoneNum, user.getPassword(), role);
}
}
}
- WebSecurity Config
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
//token的驗證方式不需要開啓csrf的防護
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
//設置無狀態的連接,即不創建session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 當前的url允許進行匿名訪問,即不需要身份認證
.antMatchers(
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
//配置swagger界面的匿名訪問
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/images/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/configuration/ui").permitAll()
.antMatchers("/configuration/security").permitAll()
//配置允許匿名訪問的路徑
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
//配置自己的驗證過濾器
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl();
}
}
- 自定義註解控制權限
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleContro {
RoleEnum role();
}
示例:
//此時只有權限爲User即註冊用戶纔有權限訪問這個接口
@RoleContro(role = RoleEnum.USER)
@PostMapping(name = "修改個人信息", value = "/updateUser")
public void updateUser() {
//TODO 用戶修改or完善個人信息
}
- UserService
/**
* 用戶服務接口
* * @author Hobo
*/
public interface UserService {
/**
* 根據手機號碼查詢用戶
*
* @param phoneNumber
* @return User
* @author hobo
*/
public User getUserByPhoneNum(String phoneNumber);
/**
* 通過token解析用戶
*
* @return User
* @author hobo
*/
public User getCurrentUser();
/***
* 用戶登錄
* @param loginForm
* @param response
* @author hobo
* @return java.lang.Object
*/
public Object login(LoginForm loginForm, HttpServletResponse response);
}
- UserService實現類
/**
* 用戶服務接口
* * @author Hobo
*/
public interface UserService {
/**
* 根據手機號碼查詢用戶
*
* @param phoneNumber
* @return User
* @author hobo
*/
public User getUserByPhoneNum(String phoneNumber);
/**
* 通過token解析用戶
*
* @return User
* @author hobo
*/
public User getCurrentUser();
/***
* 用戶登錄
* @param loginForm
* @param response
* @author hobo
* @return java.lang.Object
*/
public Object login(LoginForm loginForm, HttpServletResponse response);
}
- loginForm
@Data
public class LoginForm {
@NotNull(message = "手機號不能爲空")
@ApiModelProperty("手機號碼")
private String phoneNum;
@NotNull(message = "密碼")
private String password;
}
- LoginController
@RequestMapping("/login")
public class LoginController {
@Autowired
private UserService userService;
@PostMapping(name = "用戶登錄", value = "/login")
public Object login(LoginForm loginForm, HttpServletResponse response) {
return userService.login(loginForm, response);
}
}
如果有志同道合的朋友或者大佬指點~ 歡迎騷擾 qq:1056024860